diff --git a/.editorconfig b/.editorconfig
index f18b998a..45695edb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -212,5 +212,8 @@ dotnet_diagnostic.CSIsNull001.severity = warning
# CSIsNull002: Use `is object` for non-null checks
dotnet_diagnostic.CSIsNull002.severity = warning
+# NBMsgPack051: Prefer .NET APIs over netstandard ones.
+dotnet_diagnostic.NBMsgPack051.severity = silent
+
[*.sln]
indent_style = tab
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8fa150db..59d76879 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -21,11 +21,12 @@
+
-
+
diff --git a/docfx/docs/extensibility.md b/docfx/docs/extensibility.md
index d4e38284..64079fcc 100644
--- a/docfx/docs/extensibility.md
+++ b/docfx/docs/extensibility.md
@@ -76,13 +76,20 @@ Interop with other parties is most likely with a UTF-8 text encoding of JSON-RPC
StreamJsonRpc includes the following implementations:
-1. @StreamJsonRpc.JsonMessageFormatter - Uses Newtonsoft.Json to serialize each JSON-RPC message as actual JSON.
+1. - Uses the [`Nerdbank.MessagePack` library][NBMsgPack] to serialize
+ each message using the very fast and compact binary [MessagePack format][MessagePackFormat].
+ This serializer is NativeAOT ready.
+ Any RPC method parameters and return types that require custom serialization may provide it
+ with a `MessagePackConverter`-derived class.
+ All custom converters can be added to the serializer at `NerdbankMessagePackFormatter.UserDataSerializer`.
+
+1. - Uses Newtonsoft.Json to serialize each JSON-RPC message as actual JSON.
The text encoding is configurable via a property.
All RPC method parameters and return types must be serializable by Newtonsoft.Json.
You can leverage `JsonConverter` and add your custom converters via attributes or by
contributing them to the `JsonMessageFormatter.JsonSerializer.Converters` collection.
-1. - Uses the [MessagePack-CSharp][MessagePackLibrary] library to serialize each
+1. - Uses the [MessagePack-CSharp][MessagePackCSharp] library to serialize each
JSON-RPC message using the very fast and compact binary [MessagePack format][MessagePackFormat].
All RPC method parameters and return types must be serializable by `IMessagePackFormatter`.
You can contribute your own via `MessagePackFormatter.SetOptions(MessagePackSerializationOptions)`.
@@ -100,17 +107,17 @@ Refer to the source code from our built-in formatters to see how to use these he
### Choosing your formatter
-#### When to use
+#### When to use
-The very best performance comes from using the with the .
+The very best performance comes from using the with the .
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
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.
+The `MessagePackFormatter` is an older formatter that is not NativeAOT ready.
+Using it is only advisable for purposes of maintaining serialized format compatibility, since the serialized schema between the two MessagePack formatters varies slightly.
#### When to use
@@ -128,7 +135,8 @@ It produces JSON text and allows configuring the text encoding, with UTF-8 being
This formatter is compatible with remote systems that use when using the default UTF-8 encoding.
The remote party must also use the same message handler, such as .
-[MessagePackLibrary]: https://github.com/MessagePack-CSharp/MessagePack-CSharp
+[NBMsgPack]: https://github.com/AArnott/Nerdbank.MessagePack
+[MessagePackCSharp]: https://github.com/MessagePack-CSharp/MessagePack-CSharp
[MessagePackUsage]: https://github.com/microsoft/vs-streamjsonrpc/blob/main/test/StreamJsonRpc.Tests/MessagePackFormatterTests.cs
[MessagePackFormat]: https://msgpack.org/
[SystemTextJson]: https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/overview
diff --git a/docfx/exotic_types/asyncenumerable.md b/docfx/exotic_types/asyncenumerable.md
index c71e37e9..22c22fcc 100644
--- a/docfx/exotic_types/asyncenumerable.md
+++ b/docfx/exotic_types/asyncenumerable.md
@@ -552,6 +552,17 @@ The generator MAY respond with an error if this is done.
The generator should never return an empty array of values unless the last value in the sequence has already
been returned to the client.
+#### Compatibility note
+
+The `MessagePackFormatter` deviates from this spec by formatting the result object above as an array of values instead.
+The example above would instead be formatted as:
+
+```json
+[[4,5,6], false]
+```
+
+The `NerdbankMessagePackFormatter` does *not* share this spec bug, and thus cannot interoperate with a `MessagePackFormatter` across the wire.
+
### Consumer disposes enumerator
When the consumer aborts enumeration before the generator has sent `finished: true`,
diff --git a/nuget.config b/nuget.config
index 35d15c11..6a811cf6 100644
--- a/nuget.config
+++ b/nuget.config
@@ -5,10 +5,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs
index 7dd4c647..aa7223e9 100644
--- a/src/StreamJsonRpc/FormatterBase.cs
+++ b/src/StreamJsonRpc/FormatterBase.cs
@@ -7,6 +7,7 @@
using System.Reflection;
using System.Runtime.Serialization;
using Nerdbank.Streams;
+using PolyType;
using StreamJsonRpc.Protocol;
using StreamJsonRpc.Reflection;
@@ -436,6 +437,7 @@ protected abstract class JsonRpcErrorBase : JsonRpcError, IJsonRpcMessageBufferM
///
[Newtonsoft.Json.JsonIgnore]
[IgnoreDataMember]
+ [PropertyShape(Ignore = true)]
public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; }
void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message)
@@ -480,6 +482,7 @@ protected abstract class JsonRpcResultBase : JsonRpcResult, IJsonRpcMessageBuffe
///
[Newtonsoft.Json.JsonIgnore]
[IgnoreDataMember]
+ [PropertyShape(Ignore = true)]
public TopLevelPropertyBagBase? TopLevelPropertyBag { get; set; }
void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message)
diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs
index 8f21c5d0..355dcc9f 100644
--- a/src/StreamJsonRpc/JsonMessageFormatter.cs
+++ b/src/StreamJsonRpc/JsonMessageFormatter.cs
@@ -1403,7 +1403,7 @@ internal ExceptionConverter(JsonMessageFormatter formatter)
}
}
- return ExceptionSerializationHelpers.Deserialize(this.formatter.JsonRpc, info, this.formatter.JsonRpc?.TraceSource);
+ return ExceptionSerializationHelpers.Deserialize(this.formatter.JsonRpc, info, this.formatter.JsonRpc.LoadType, this.formatter.JsonRpc?.TraceSource);
}
finally
{
diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs
index 3047c7be..aaad1efa 100644
--- a/src/StreamJsonRpc/JsonRpc.cs
+++ b/src/StreamJsonRpc/JsonRpc.cs
@@ -8,6 +8,7 @@
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
using Microsoft.VisualStudio.Threading;
using Newtonsoft.Json;
using StreamJsonRpc.Protocol;
@@ -38,6 +39,11 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
///
private static readonly JsonRpcError DroppedError = new();
+ private static readonly ImmutableDictionary DefaultRuntimeDeserializableTypes = ImmutableDictionary.Create()
+ .Add("System.Exception", new LoadableType(typeof(Exception)))
+ .Add("System.ArgumentException", new LoadableType(typeof(ArgumentException)))
+ .Add("System.InvalidOperationException", new LoadableType(typeof(InvalidOperationException)));
+
#if NET
private static readonly MethodInfo ValueTaskAsTaskMethodInfo = typeof(ValueTask<>).GetMethod(nameof(ValueTask.AsTask))!;
private static readonly MethodInfo ValueTaskGetResultMethodInfo = typeof(ValueTask<>).GetMethod("get_Result")!;
@@ -99,6 +105,8 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
///
private ImmutableList remoteRpcTargets = ImmutableList.Empty;
+ // TODO: make this a custom collection type so it can be shared across JsonRpc instances.
+ private ImmutableDictionary runtimeDeserializableTypes = DefaultRuntimeDeserializableTypes;
private Task? readLinesTask;
private long nextId = 1;
private int requestsInDispatchCount;
@@ -251,6 +259,10 @@ public JsonRpc(IJsonRpcMessageHandler messageHandler)
// so that all incoming messages are queued to the threadpool, allowing immediate concurrency.
this.SynchronizationContext = new NonConcurrentSynchronizationContext(sticky: false);
this.CancellationStrategy = new StandardCancellationStrategy(this);
+
+ this.AddLoadableType(typeof(Exception));
+ this.AddLoadableType(typeof(InvalidOperationException));
+ this.AddLoadableType(typeof(ArgumentException));
}
///
@@ -435,6 +447,11 @@ public enum TraceEvents
/// A base-type that does offer the constructor will be instantiated instead.
///
ExceptionNotDeserializable,
+
+ ///
+ /// An error occurred while deserializing a value within an interface.
+ ///
+ IFormatterConverterDeserializationFailure,
}
///
@@ -939,6 +956,24 @@ public void AddLocalRpcMethod(string? rpcMethodName, Delegate handler)
return this.rpcTargetInfo.GetJsonRpcMethodAttribute(methodName, parameters);
}
+ ///
+ /// Gets or sets the set of types that can be deserialized from their name at runtime.
+ ///
+ ///
+ /// This set of types is used by the default implementation of to determine
+ /// which types can be deserialized when their name is encountered in an RPC message.
+ ///
+ public void AddLoadableType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type)
+ {
+ Requires.NotNull(type);
+ Requires.Argument(type.FullName is not null, nameof(type), Resources.TypeMustHaveFullName);
+ this.ThrowIfConfigurationLocked();
+ lock (this.syncObject)
+ {
+ this.runtimeDeserializableTypes = this.runtimeDeserializableTypes.SetItem(type.FullName, new LoadableType(type));
+ }
+ }
+
///
/// Starts listening to incoming messages.
///
@@ -1311,6 +1346,7 @@ internal void AddRpcInterfaceToTargetInternal([DynamicallyAccessedMembers(Dynami
/// Implementations should avoid throwing , or other exceptions, preferring to return instead.
///
[RequiresUnreferencedCode(RuntimeReasons.LoadType)]
+ [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
protected internal virtual Type? LoadType(string typeFullName, string? assemblyName)
{
Requires.NotNull(typeFullName, nameof(typeFullName));
@@ -1345,6 +1381,25 @@ internal void AddRpcInterfaceToTargetInternal([DynamicallyAccessedMembers(Dynami
return runtimeType;
}
+ ///
+ /// When overridden by a derived type, this attempts to load a type based on its full name and possibly assembly name.
+ ///
+ /// The of the type to be loaded.
+ /// The assemble name that is expected to define the type, if available. This should be parseable by .
+ /// The loaded , if one could be found; otherwise .
+ ///
+ ///
+ /// This method is used to load types that are strongly referenced by incoming messages during serialization.
+ /// It is important to not load types that may pose a security threat based on the type and the trust level of the remote party.
+ ///
+ ///
+ /// The default implementation of this method matches types registered with .
+ ///
+ /// Implementations should avoid throwing , or other exceptions, preferring to return instead.
+ ///
+ [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
+ protected internal virtual Type? LoadTypeTrimSafe(string typeFullName, string? assemblyName) => this.runtimeDeserializableTypes.TryGetValue(typeFullName, out LoadableType type) ? type.Type : null;
+
///
/// Disposes managed and native resources held by this instance.
///
@@ -1392,7 +1447,11 @@ protected virtual JsonRpcError.ErrorDetail CreateErrorDetails(JsonRpcRequest req
bool iserializable = this.ExceptionStrategy == ExceptionProcessing.ISerializable;
if (!ExceptionSerializationHelpers.IsSerializable(exception))
{
- this.TraceSource.TraceEvent(TraceEventType.Warning, (int)TraceEvents.ExceptionNotSerializable, "An exception of type {0} was thrown but is not serializable.", exception.GetType().AssemblyQualifiedName);
+ if (localRpcEx is null)
+ {
+ this.TraceSource.TraceEvent(TraceEventType.Warning, (int)TraceEvents.ExceptionNotSerializable, "An exception of type {0} was thrown but is not serializable.", exception.GetType().AssemblyQualifiedName);
+ }
+
iserializable = false;
}
@@ -1737,7 +1796,7 @@ protected virtual async ValueTask DispatchRequestAsync(JsonRpcRe
}
///
- /// Sends the JSON-RPC message to intance to be transmitted.
+ /// Sends the JSON-RPC message to instance to be transmitted.
///
/// The message to send.
/// A token to cancel the send request.
@@ -2825,6 +2884,18 @@ private IJsonRpcClientProxyInternal CreateProxy(Type contractInterface, ReadOnly
options?.OnDispose)!;
}
+ private struct LoadableType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) : IEquatable
+ {
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
+ public Type Type => type;
+
+ public bool Equals(LoadableType other) => this.Type == other.Type;
+
+ public override int GetHashCode() => type.GetHashCode();
+
+ public override bool Equals(object? obj) => obj is LoadableType other && this.Equals(other);
+ }
+
///
/// An object that correlates tokens within and between instances
/// within a process that does not use ,
diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs
index d2aa723d..b34d073b 100644
--- a/src/StreamJsonRpc/MessagePackFormatter.cs
+++ b/src/StreamJsonRpc/MessagePackFormatter.cs
@@ -1445,7 +1445,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.JsonRpc, info, resolverWrapper?.Formatter.JsonRpc?.TraceSource);
+ return ExceptionSerializationHelpers.Deserialize(this.formatter.JsonRpc, info, this.formatter.JsonRpc.LoadType, resolverWrapper?.Formatter.JsonRpc?.TraceSource);
}
finally
{
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.AsyncEnumerableConverters.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.AsyncEnumerableConverters.cs
new file mode 100644
index 00000000..01e7c402
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.AsyncEnumerableConverters.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+#pragma warning disable CA1812 // Avoid uninstantiated internal classes
+
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Nodes;
+using Nerdbank.MessagePack;
+using PolyType;
+using PolyType.Abstractions;
+using StreamJsonRpc;
+using StreamJsonRpc.Reflection;
+
+[assembly: TypeShapeExtension(typeof(IAsyncEnumerable<>), AssociatedTypes = [typeof(NerdbankMessagePackFormatter.AsyncEnumerableConverter<>)], Requirements = TypeShapeRequirements.Constructor)]
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ ///
+ /// Converts between an enumeration token and .
+ ///
+ /// The type of element to be enumerated.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public class AsyncEnumerableConverter : MessagePackConverter>
+ {
+ ///
+ /// The constant "token", in its various forms.
+ ///
+ private static readonly MessagePackString TokenPropertyName = new(MessageFormatterEnumerableTracker.TokenPropertyName);
+
+ ///
+ /// The constant "values", in its various forms.
+ ///
+ private static readonly MessagePackString ValuesPropertyName = new(MessageFormatterEnumerableTracker.ValuesPropertyName);
+
+ ///
+ public override IAsyncEnumerable? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ if (reader.TryReadNil())
+ {
+ return default;
+ }
+
+ NerdbankMessagePackFormatter mainFormatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ RawMessagePack? token = default;
+ IReadOnlyList? initialElements = null;
+ int propertyCount = reader.ReadMapHeader();
+ for (int i = 0; i < propertyCount; i++)
+ {
+ if (TokenPropertyName.TryRead(ref reader))
+ {
+ // The value needs to outlive the reader, so we clone it.
+ token = reader.ReadRaw(context).ToOwned();
+ }
+ else if (ValuesPropertyName.TryRead(ref reader))
+ {
+ initialElements = context.GetConverter>(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else
+ {
+ reader.Skip(context); // Skip the unrecognized key
+ reader.Skip(context); // and its value.
+ }
+ }
+
+ return mainFormatter.EnumerableTracker.CreateEnumerableProxy(token.HasValue ? token.Value : null, initialElements);
+ }
+
+ ///
+ [SuppressMessage("Usage", "NBMsgPack031:Converters should read or write exactly one msgpack structure", Justification = "Writer is passed to helper method")]
+ public override void Write(ref MessagePackWriter writer, in IAsyncEnumerable? value, SerializationContext context)
+ {
+ context.DepthStep();
+
+ NerdbankMessagePackFormatter mainFormatter = context.GetFormatter();
+ if (value is null)
+ {
+ writer.WriteNil();
+ }
+ else
+ {
+ (IReadOnlyList elements, bool finished) = value.TearOffPrefetchedElements();
+ long token = mainFormatter.EnumerableTracker.GetToken(value);
+
+ int propertyCount = 0;
+ if (elements.Count > 0)
+ {
+ propertyCount++;
+ }
+
+ if (!finished)
+ {
+ propertyCount++;
+ }
+
+ writer.WriteMapHeader(propertyCount);
+
+ if (!finished)
+ {
+ writer.Write(TokenPropertyName);
+ writer.Write(token);
+ }
+
+ if (elements.Count > 0)
+ {
+ writer.Write(ValuesPropertyName);
+ context.GetConverter(mainFormatter.GetUserDataShape(typeof(IReadOnlyList))).WriteObject(ref writer, elements, context);
+ }
+ }
+ }
+
+ ///
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.Constants.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.Constants.cs
new file mode 100644
index 00000000..c7cd1db3
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.Constants.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Nerdbank.MessagePack;
+using StreamJsonRpc.Protocol;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ ///
+ /// The constant "jsonrpc".
+ ///
+ private static readonly MessagePackString VersionPropertyName = new(Constants.jsonrpc);
+
+ ///
+ /// The constant "id".
+ ///
+ private static readonly MessagePackString IdPropertyName = new(Constants.id);
+
+ ///
+ /// The constant "method".
+ ///
+ private static readonly MessagePackString MethodPropertyName = new(Constants.Request.method);
+
+ ///
+ /// The constant "result".
+ ///
+ private static readonly MessagePackString ResultPropertyName = new(Constants.Result.result);
+
+ ///
+ /// The constant "error".
+ ///
+ private static readonly MessagePackString ErrorPropertyName = new(Constants.Error.error);
+
+ ///
+ /// The constant "params".
+ ///
+ private static readonly MessagePackString ParamsPropertyName = new(Constants.Request.@params);
+
+ ///
+ /// The constant "traceparent".
+ ///
+ private static readonly MessagePackString TraceParentPropertyName = new(Constants.Request.traceparent);
+
+ ///
+ /// The constant "tracestate".
+ ///
+ private static readonly MessagePackString TraceStatePropertyName = new(Constants.Request.tracestate);
+
+ ///
+ /// The constant "2.0".
+ ///
+ private static readonly MessagePackString Version2 = new("2.0");
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.ExceptionConverter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.ExceptionConverter.cs
new file mode 100644
index 00000000..d6f820f6
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.ExceptionConverter.cs
@@ -0,0 +1,126 @@
+// 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.Nodes;
+using Nerdbank.MessagePack;
+using PolyType.Abstractions;
+using StreamJsonRpc.Reflection;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ ///
+ /// Manages serialization of any -derived type that follows standard rules.
+ ///
+ ///
+ /// A serializable class will:
+ /// 1. Derive from
+ /// 2. Be attributed with
+ /// 3. Declare a constructor with a signature of (, ).
+ ///
+ private class ExceptionConverter : MessagePackConverter
+ {
+ public static readonly ExceptionConverter Instance = new();
+
+ public override T? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+ Assumes.NotNull(formatter.JsonRpc);
+
+ context.DepthStep();
+
+ if (reader.TryReadNil())
+ {
+ return default;
+ }
+
+ // 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.
+ formatter.exceptionRecursionCounter.Value++;
+ try
+ {
+ if (formatter.exceptionRecursionCounter.Value > 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(context);
+ return default;
+ }
+
+ var info = new SerializationInfo(typeof(T), new MessagePackFormatterConverter(formatter));
+ int memberCount = reader.ReadMapHeader();
+ for (int i = 0; i < memberCount; i++)
+ {
+ string? name = context.GetConverter(context.TypeShapeProvider).Read(ref reader, context)
+ ?? throw new MessagePackSerializationException(Resources.UnexpectedNullValueInMap);
+
+ // SerializationInfo.GetValue(string, typeof(object)) does not call our formatter,
+ // so the caller will get a boxed RawMessagePack struct in that case.
+ // Although we can't do much about *that* in general, we can at least ensure that null values
+ // are represented as null instead of this boxed struct.
+ RawMessagePack? value = reader.TryReadNil() ? null : reader.ReadRaw(context);
+
+ info.AddSafeValue(name, value);
+ }
+
+ return ExceptionSerializationHelpers.Deserialize(formatter.JsonRpc, info, formatter.JsonRpc.LoadTypeTrimSafe, formatter.JsonRpc.TraceSource);
+ }
+ finally
+ {
+ formatter.exceptionRecursionCounter.Value--;
+ }
+ }
+
+ public override void Write(ref MessagePackWriter writer, in T? value, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+ if (value is null)
+ {
+ writer.WriteNil();
+ return;
+ }
+
+ formatter.exceptionRecursionCounter.Value++;
+ try
+ {
+ if (formatter.exceptionRecursionCounter.Value > 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();
+ return;
+ }
+
+ var info = new SerializationInfo(typeof(T), new MessagePackFormatterConverter(formatter));
+ ExceptionSerializationHelpers.Serialize((Exception)(object)value, info);
+ writer.WriteMapHeader(info.GetSafeMemberCount());
+ foreach (SerializationEntry element in info.GetSafeMembers())
+ {
+ writer.Write(element.Name);
+ if (element.Value is null)
+ {
+ writer.WriteNil();
+ }
+ else
+ {
+ // We prefer the declared type but will fallback to the runtime type.
+ context.GetConverter(formatter.TypeShapeProvider.GetShape(NormalizeType(element.ObjectType)) ?? formatter.TypeShapeProvider.Resolve(NormalizeType(element.Value.GetType())))
+ .WriteObject(ref writer, element.Value, context);
+ }
+ }
+ }
+ finally
+ {
+ formatter.exceptionRecursionCounter.Value--;
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.IJsonRpcMessagePackRetention.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.IJsonRpcMessagePackRetention.cs
new file mode 100644
index 00000000..6e86c7a9
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.IJsonRpcMessagePackRetention.cs
@@ -0,0 +1,24 @@
+// 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 Nerdbank.MessagePack;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ private interface IJsonRpcMessagePackRetention
+ {
+ ///
+ /// Gets the original msgpack sequence that was deserialized into this message.
+ ///
+ ///
+ /// The buffer is only retained for a short time. If it has already been cleared, the result of this property is an empty sequence.
+ ///
+ RawMessagePack OriginalMessagePack { get; }
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.MessagePackFormatterConverter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.MessagePackFormatterConverter.cs
new file mode 100644
index 00000000..2b0cfc3e
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.MessagePackFormatterConverter.cs
@@ -0,0 +1,101 @@
+// 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.Runtime.Serialization;
+using Nerdbank.MessagePack;
+using PolyType;
+using StreamJsonRpc.Reflection;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ private partial class MessagePackFormatterConverter(NerdbankMessagePackFormatter formatter) : IFormatterConverter
+ {
+#pragma warning disable CS8766 // This method may in fact return null, and no one cares.
+ public object? Convert(object value, Type type)
+#pragma warning restore CS8766
+ {
+ // We don't support serializing/deserializing the non-generic IDictionary,
+ // since it uses untyped keys and values which we cannot securely hash.
+ if (type == typeof(System.Collections.IDictionary))
+ {
+ // Force us to deserialize into a semi-typed dictionary.
+ // The string key is a reasonable 99% compatible assumption, and allows us to securely hash the keys.
+ // The untyped values will be alright because we support the primitives types.
+ type = typeof(Dictionary);
+ }
+
+ MessagePackReader reader = this.CreateReader(value);
+ try
+ {
+ return formatter.UserDataSerializer.DeserializeObject(ref reader, formatter.GetUserDataShape(type));
+ }
+ catch (Exception ex)
+ {
+ formatter.JsonRpc?.TraceSource.TraceData(TraceEventType.Error, (int)JsonRpc.TraceEvents.ExceptionNotDeserializable, ex);
+ throw;
+ }
+ }
+
+ public object Convert(object value, TypeCode typeCode) => typeCode switch
+ {
+ TypeCode.Object => new object(),
+ _ => ExceptionSerializationHelpers.Convert(this, value, typeCode),
+ };
+
+ public bool ToBoolean(object value) => this.CreateReader(value).ReadBoolean();
+
+ public byte ToByte(object value) => this.CreateReader(value).ReadByte();
+
+ public char ToChar(object value) => this.CreateReader(value).ReadChar();
+
+ public DateTime ToDateTime(object value) => this.CreateReader(value).ReadDateTime();
+
+ public decimal ToDecimal(object value) => formatter.UserDataSerializer.Deserialize((RawMessagePack)value, Witness.ShapeProvider);
+
+ public double ToDouble(object value) => this.CreateReader(value).ReadDouble();
+
+ public short ToInt16(object value) => this.CreateReader(value).ReadInt16();
+
+ public int ToInt32(object value) => this.CreateReader(value).ReadInt32();
+
+ public long ToInt64(object value) => this.CreateReader(value).ReadInt64();
+
+ public sbyte ToSByte(object value) => this.CreateReader(value).ReadSByte();
+
+ public float ToSingle(object value) => this.CreateReader(value).ReadSingle();
+
+ public string? ToString(object value) => value is null ? null : this.CreateReader(value).ReadString();
+
+ public ushort ToUInt16(object value) => this.CreateReader(value).ReadUInt16();
+
+ public uint ToUInt32(object value) => this.CreateReader(value).ReadUInt32();
+
+ public ulong ToUInt64(object value) => this.CreateReader(value).ReadUInt64();
+
+ private MessagePackReader CreateReader(object value) => new((RawMessagePack)value);
+
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor]
+ [GenerateShapeFor>]
+ private partial class Witness;
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.PipeConverters.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.PipeConverters.cs
new file mode 100644
index 00000000..39afed88
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.PipeConverters.cs
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.IO.Pipelines;
+using System.Text.Json.Nodes;
+using Nerdbank.MessagePack;
+using Nerdbank.Streams;
+using PolyType.Abstractions;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ private static class PipeConverters
+ {
+ internal class DuplexPipeConverter : MessagePackConverter
+ {
+ public static readonly DuplexPipeConverter DefaultInstance = new();
+
+ public override IDuplexPipe? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ if (reader.TryReadNil())
+ {
+ return null;
+ }
+
+ return formatter.DuplexPipeTracker.GetPipe(reader.ReadUInt64());
+ }
+
+ public override void Write(ref MessagePackWriter writer, in IDuplexPipe? value, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ if (formatter.DuplexPipeTracker.GetULongToken(value) is ulong token)
+ {
+ writer.Write(token);
+ }
+ else
+ {
+ writer.WriteNil();
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+
+ internal class PipeReaderConverter : MessagePackConverter
+ {
+ public static readonly PipeReaderConverter DefaultInstance = new();
+
+ public override PipeReader? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+ if (reader.TryReadNil())
+ {
+ return null;
+ }
+
+ return formatter.DuplexPipeTracker.GetPipeReader(reader.ReadUInt64());
+ }
+
+ public override void Write(ref MessagePackWriter writer, in PipeReader? value, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+ if (formatter.DuplexPipeTracker.GetULongToken(value) is { } token)
+ {
+ writer.Write(token);
+ }
+ else
+ {
+ writer.WriteNil();
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+
+ internal class PipeWriterConverter : MessagePackConverter
+ {
+ public static readonly PipeWriterConverter DefaultInstance = new();
+
+ public override PipeWriter? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+ if (reader.TryReadNil())
+ {
+ return null;
+ }
+
+ return formatter.DuplexPipeTracker.GetPipeWriter(reader.ReadUInt64());
+ }
+
+ public override void Write(ref MessagePackWriter writer, in PipeWriter? value, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+ if (formatter.DuplexPipeTracker.GetULongToken(value) is ulong token)
+ {
+ writer.Write(token);
+ }
+ else
+ {
+ writer.WriteNil();
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+
+ internal class StreamConverter : MessagePackConverter
+ {
+ public static readonly StreamConverter DefaultInstance = new();
+
+ public override Stream? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+ if (reader.TryReadNil())
+ {
+ return null;
+ }
+
+ return formatter.DuplexPipeTracker.GetPipe(reader.ReadUInt64()).AsStream();
+ }
+
+ public override void Write(ref MessagePackWriter writer, in Stream? value, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+ if (formatter.DuplexPipeTracker.GetULongToken(value?.UsePipe()) is { } token)
+ {
+ writer.Write(token);
+ }
+ else
+ {
+ writer.WriteNil();
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.ProgressConverters.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.ProgressConverters.cs
new file mode 100644
index 00000000..818ce534
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.ProgressConverters.cs
@@ -0,0 +1,70 @@
+// 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.CodeAnalysis;
+using System.Text.Json.Nodes;
+using Nerdbank.MessagePack;
+using PolyType.Abstractions;
+using StreamJsonRpc.Reflection;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ ///
+ /// Converts a progress token to an or an into a token.
+ ///
+ /// The closed interface.
+ private class FullProgressConverter : MessagePackConverter
+ {
+ private Func? progressProxyCtor;
+
+ [return: MaybeNull]
+ public override TClass? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ if (reader.TryReadNil())
+ {
+ return default;
+ }
+
+ Assumes.NotNull(formatter.JsonRpc);
+ RawMessagePack token = reader.ReadRaw(context).ToOwned();
+ bool clientRequiresNamedArgs = formatter.ApplicableMethodAttributeOnDeserializingMethod?.ClientRequiresNamedArguments is true;
+
+ if (this.progressProxyCtor is null)
+ {
+ ITypeShape typeShape = context.TypeShapeProvider?.Resolve(typeof(TClass)) ?? throw new InvalidOperationException("No TypeShapeProvider available.");
+ IObjectTypeShape progressProxyShape = (IObjectTypeShape?)typeShape.GetAssociatedTypeShape(typeof(MessageFormatterProgressTracker.ProgressProxy<>)) ?? throw new InvalidOperationException("Unable to get ProgressProxy associated shape.");
+ this.progressProxyCtor = (Func?)progressProxyShape.Constructor?.Accept(NonDefaultConstructorVisitor.Instance) ?? throw new InvalidOperationException("Unable to construct IProgress proxy.");
+ }
+
+ return this.progressProxyCtor(formatter.JsonRpc, token, clientRequiresNamedArgs);
+ }
+
+ public override void Write(ref MessagePackWriter writer, in TClass? value, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ if (value is null)
+ {
+ writer.WriteNil();
+ }
+ else
+ {
+ long progressId = formatter.FormatterProgressTracker.GetTokenForProgress(value);
+ writer.Write(progressId);
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.RequestIdConverter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.RequestIdConverter.cs
new file mode 100644
index 00000000..0ac3303a
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.RequestIdConverter.cs
@@ -0,0 +1,58 @@
+// 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.Nodes;
+using Nerdbank.MessagePack;
+using PolyType.Abstractions;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ private class RequestIdConverter : MessagePackConverter
+ {
+ internal static readonly RequestIdConverter Instance = new();
+
+ private RequestIdConverter()
+ {
+ }
+
+ public override RequestId Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ context.DepthStep();
+
+ if (reader.NextMessagePackType == MessagePackType.Integer)
+ {
+ return new RequestId(reader.ReadInt64());
+ }
+ else
+ {
+ // Do *not* read as an interned string here because this ID should be unique.
+ return new RequestId(reader.ReadString());
+ }
+ }
+
+ public override void Write(ref MessagePackWriter writer, in RequestId value, SerializationContext context)
+ {
+ context.DepthStep();
+
+ if (value.Number.HasValue)
+ {
+ writer.Write(value.Number.Value);
+ }
+ else
+ {
+ writer.Write(value.String);
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => JsonNode.Parse("""
+ {
+ "type": ["string", "integer"]
+ }
+ """)?.AsObject();
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.RpcMarshalableConverter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.RpcMarshalableConverter.cs
new file mode 100644
index 00000000..5417956e
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.RpcMarshalableConverter.cs
@@ -0,0 +1,60 @@
+// 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.CodeAnalysis;
+using System.Text.Json.Nodes;
+using Nerdbank.MessagePack;
+using PolyType.Abstractions;
+using StreamJsonRpc.Reflection;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ private class RpcMarshalableConverter(
+ JsonRpcProxyOptions proxyOptions,
+ JsonRpcTargetOptions targetOptions,
+ RpcMarshalableAttribute rpcMarshalableAttribute) : MessagePackConverter
+ ////where T : class // We expect this, but requiring it adds a constraint that some callers cannot statically satisfy.
+ {
+ [SuppressMessage("Usage", "NBMsgPack031:Converters should read or write exactly one msgpack structure", Justification = "Reader is passed to rpc context")]
+ public override T? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+#pragma warning disable NBMsgPack030 // Converters should not call top-level `MessagePackSerializer` methods - We need to switch from user data to envelope serializer
+ MessageFormatterRpcMarshaledContextTracker.MarshalToken? token = formatter
+ .envelopeSerializer.Deserialize(ref reader, Witness.ShapeProvider, context.CancellationToken);
+#pragma warning restore NBMsgPack030 // Converters should not call top-level `MessagePackSerializer` methods
+
+ return token.HasValue
+ ? (T?)formatter.RpcMarshaledContextTracker.GetObject(typeof(T), token, proxyOptions)
+ : default;
+ }
+
+ [SuppressMessage("Usage", "NBMsgPack031:Converters should read or write exactly one msgpack structure", Justification = "Writer is passed to rpc context")]
+ public override void Write(ref MessagePackWriter writer, in T? value, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ if (value is null)
+ {
+ writer.WriteNil();
+ }
+ else
+ {
+ MessageFormatterRpcMarshaledContextTracker.MarshalToken token = formatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, typeof(T), rpcMarshalableAttribute);
+ context.GetConverter(Witness.ShapeProvider).Write(ref writer, token, context);
+ }
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.ToStringHelper.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.ToStringHelper.cs
new file mode 100644
index 00000000..9baa5c80
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.ToStringHelper.cs
@@ -0,0 +1,51 @@
+// 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 Nerdbank.MessagePack;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ ///
+ /// A recyclable object that can serialize a message to JSON on demand.
+ ///
+ ///
+ /// 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 to be reused,
+ /// since its lifetime is only required during a synchronous call to a trace API.
+ ///
+ private class ToStringHelper
+ {
+ private RawMessagePack? 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.");
+
+ return this.jsonString ??= MessagePackSerializer.ConvertToJson(this.encodedMessage.Value);
+ }
+
+ ///
+ /// Initializes this object to represent a message.
+ ///
+ internal void Activate(RawMessagePack 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/NerdbankMessagePackFormatter.TraceParentConverter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.TraceParentConverter.cs
new file mode 100644
index 00000000..39b31939
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.TraceParentConverter.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Buffers;
+using System.Text.Json.Nodes;
+using Nerdbank.MessagePack;
+using PolyType.Abstractions;
+using StreamJsonRpc.Protocol;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+public partial class NerdbankMessagePackFormatter
+{
+ internal class TraceParentConverter : MessagePackConverter
+ {
+ public unsafe override TraceParent Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ context.DepthStep();
+
+ if (reader.ReadArrayHeader() != 2)
+ {
+ throw new NotSupportedException("Unexpected array length.");
+ }
+
+ var result = default(TraceParent);
+ result.Version = reader.ReadByte();
+ if (result.Version != 0)
+ {
+ throw new NotSupportedException("traceparent version " + result.Version + " is not supported.");
+ }
+
+ if (reader.ReadArrayHeader() != 3)
+ {
+ throw new NotSupportedException("Unexpected array length in version-format.");
+ }
+
+ ReadOnlySequence bytes = reader.ReadBytes() ?? throw new NotSupportedException("Expected traceid not found.");
+ bytes.CopyTo(new Span(result.TraceId, TraceParent.TraceIdByteCount));
+
+ bytes = reader.ReadBytes() ?? throw new NotSupportedException("Expected parentid not found.");
+ bytes.CopyTo(new Span(result.ParentId, TraceParent.ParentIdByteCount));
+
+ result.Flags = (TraceParent.TraceFlags)reader.ReadByte();
+
+ return result;
+ }
+
+ public unsafe override void Write(ref MessagePackWriter writer, in TraceParent value, SerializationContext context)
+ {
+ if (value.Version != 0)
+ {
+ throw new NotSupportedException("traceparent version " + value.Version + " is not supported.");
+ }
+
+ context.DepthStep();
+
+ writer.WriteArrayHeader(2);
+
+ writer.Write(value.Version);
+
+ writer.WriteArrayHeader(3);
+
+ fixed (byte* traceId = value.TraceId)
+ {
+ writer.Write(new ReadOnlySpan(traceId, TraceParent.TraceIdByteCount));
+ }
+
+ fixed (byte* parentId = value.ParentId)
+ {
+ writer.Write(new ReadOnlySpan(parentId, TraceParent.ParentIdByteCount));
+ }
+
+ writer.Write((byte)value.Flags);
+ }
+
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs
new file mode 100644
index 00000000..25e9ef5c
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs
@@ -0,0 +1,1447 @@
+// 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;
+using System.Collections.Immutable;
+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.Nodes;
+using Nerdbank.MessagePack;
+using PolyType;
+using PolyType.Abstractions;
+using StreamJsonRpc.Protocol;
+using StreamJsonRpc.Reflection;
+
+namespace StreamJsonRpc;
+
+///
+/// Serializes JSON-RPC messages using MessagePack (a fast, compact binary format).
+///
+///
+///
+/// The MessagePack implementation used here comes from https://github.com/AArnott/Nerdbank.MessagePack.
+///
+///
+/// This formatter prioritizes being trim and NativeAOT safe. As such, it uses instead of to load exception types to be deserialized.
+/// This trim-friendly method should be overridden to return types that are particularly interesting to the application.
+///
+///
+public partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcFormatterTracingCallbacks, IJsonRpcMessageFactory
+{
+ ///
+ /// The default serializer to use for user data, and a good basis for any custom values for
+ /// .
+ ///
+ ///
+ ///
+ /// This serializer is configured with set to
+ /// and various and .
+ ///
+ ///
+ /// When deviating from this default, doing so while preserving the converters and converter factories from the default
+ /// is highly recommended.
+ /// It should be done once and stored in a field and reused for the lifetime of the application
+ /// to avoid repeated startup costs associated with building up the converter tree.
+ ///
+ ///
+ public static readonly MessagePackSerializer DefaultSerializer = new MessagePackSerializer()
+ {
+ InternStrings = true,
+ ConverterFactories = [ConverterFactory.Instance],
+ Converters =
+ [
+ GetRpcMarshalableConverter(),
+ PipeConverters.PipeReaderConverter.DefaultInstance,
+ PipeConverters.PipeWriterConverter.DefaultInstance,
+ PipeConverters.DuplexPipeConverter.DefaultInstance,
+ PipeConverters.StreamConverter.DefaultInstance,
+
+ // We preset this one in user data because $/cancellation methods can carry RequestId values as arguments.
+ RequestIdConverter.Instance,
+
+ ExceptionConverter.Instance,
+ ],
+ }.WithObjectConverter();
+
+ ///
+ /// A cache of property names to declared property types, indexed by their containing parameter object type.
+ ///
+ ///
+ /// All access to this field should be while holding a lock on this member's value.
+ ///
+ private static readonly Dictionary> ParameterObjectPropertyTypes = [];
+
+ ///
+ /// The serializer context to use for top-level RPC messages.
+ ///
+ private readonly MessagePackSerializer envelopeSerializer;
+
+ private readonly ToStringHelper serializationToStringHelper = new();
+
+ private readonly ToStringHelper deserializationToStringHelper = new();
+
+ ///
+ /// Tracks recursion count while serializing or deserializing an exception.
+ ///
+ ///
+ /// This is placed here (outside the generic class)
+ /// so that it's one counter shared across all exception types that may be serialized or deserialized.
+ ///
+ private readonly ThreadLocal exceptionRecursionCounter = new();
+
+ ///
+ /// The serializer to use for user data (e.g. arguments, return values and errors).
+ ///
+ private MessagePackSerializer userDataSerializer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NerdbankMessagePackFormatter()
+ {
+ // Set up initial options for our own message types.
+ this.envelopeSerializer = DefaultSerializer with
+ {
+ StartingContext = new SerializationContext()
+ {
+ [SerializationContextExtensions.FormatterKey] = this,
+ },
+ };
+
+ // Create a serializer for user data.
+ // At the moment, we just reuse the same serializer for envelope and user data.
+ this.userDataSerializer = this.envelopeSerializer;
+ }
+
+ ///
+ /// Gets the shape provider for user data types.
+ ///
+ public required ITypeShapeProvider TypeShapeProvider { get; init; }
+
+ ///
+ /// Gets the configured serializer to use for request arguments, result values and error data.
+ ///
+ ///
+ /// When setting this property, basing the new value on is highly recommended.
+ ///
+ public MessagePackSerializer UserDataSerializer
+ {
+ get => this.userDataSerializer;
+ [MemberNotNull(nameof(this.userDataSerializer))]
+ init
+ {
+ Requires.NotNull(value);
+
+ // Customizing the input serializer to set the FormatterKey is necessary for our stateful converters.
+ // Doing this does NOT destroy the converter graph that may be cached because mutating the StartingContext
+ // property does not invalidate the graph.
+ this.userDataSerializer = value with
+ {
+ StartingContext = new SerializationContext()
+ {
+ [SerializationContextExtensions.FormatterKey] = this,
+ },
+ };
+ }
+ }
+
+ ///
+ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer)
+ {
+ JsonRpcMessage message = this.envelopeSerializer.Deserialize(contentBuffer, Witness.ShapeProvider)
+ ?? throw new MessagePackSerializationException("Failed to deserialize JSON-RPC message.");
+
+ IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc;
+ this.deserializationToStringHelper.Activate((RawMessagePack)contentBuffer);
+ try
+ {
+ tracingCallbacks?.OnMessageDeserialized(message, this.deserializationToStringHelper);
+ }
+ finally
+ {
+ this.deserializationToStringHelper.Deactivate();
+ }
+
+ return message;
+ }
+
+ ///
+ public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message)
+ {
+ if (message is Protocol.JsonRpcRequest { ArgumentsList: null, Arguments: not null and not IReadOnlyDictionary } request)
+ {
+ // This request contains named arguments, but not using a standard dictionary.
+ // Convert it to a dictionary so that the parameters can be matched to the method we're invoking.
+ if (GetParamsObjectDictionary(request.Arguments) is { } namedArgs)
+ {
+ request.Arguments = namedArgs.ArgumentValues;
+ request.NamedArgumentDeclaredTypes = namedArgs.ArgumentTypes;
+ }
+ }
+
+ var writer = new MessagePackWriter(bufferWriter);
+ try
+ {
+ this.envelopeSerializer.Serialize(ref writer, message, Witness.ShapeProvider);
+ writer.Flush();
+ }
+ catch (Exception ex)
+ {
+ throw new MessagePackSerializationException(string.Format(CultureInfo.CurrentCulture, Resources.ErrorWritingJsonRpcMessage, ex.GetType().Name, ex.Message), ex);
+ }
+ }
+
+ ///
+ public object GetJsonText(JsonRpcMessage message) => message is IJsonRpcMessagePackRetention retainedMsgPack
+ ? MessagePackSerializer.ConvertToJson(retainedMsgPack.OriginalMessagePack)
+ : throw new NotSupportedException();
+
+ ///
+ Protocol.JsonRpcRequest IJsonRpcMessageFactory.CreateRequestMessage() => new OutboundJsonRpcRequest(this);
+
+ ///
+ Protocol.JsonRpcError IJsonRpcMessageFactory.CreateErrorMessage() => new JsonRpcError(this);
+
+ ///
+ Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this);
+
+ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage message, ReadOnlySequence encodedMessage)
+ {
+ IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc;
+ this.serializationToStringHelper.Activate((RawMessagePack)encodedMessage);
+ try
+ {
+ tracingCallbacks?.OnMessageSerialized(message, this.serializationToStringHelper);
+ }
+ finally
+ {
+ this.serializationToStringHelper.Deactivate();
+ }
+ }
+
+ internal static MessagePackConverter GetRpcMarshalableConverter()
+ where T : class
+ {
+ if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(
+ typeof(T),
+ out JsonRpcProxyOptions? proxyOptions,
+ out JsonRpcTargetOptions? targetOptions,
+ out RpcMarshalableAttribute? attribute))
+ {
+ return new RpcMarshalableConverter(proxyOptions, targetOptions, attribute);
+ }
+
+ throw new NotSupportedException($"Type '{typeof(T).FullName}' is not supported for RPC Marshaling.");
+ }
+
+ ///
+ /// Extracts a dictionary of property names and values from the specified params object.
+ ///
+ /// The params object.
+ /// A dictionary of argument values and another of declared argument types, or if is null.
+ ///
+ /// This method supports DataContractSerializer-compliant types. This includes C# anonymous types.
+ ///
+ [return: NotNullIfNotNull(nameof(paramsObject))]
+ private static (IReadOnlyDictionary ArgumentValues, IReadOnlyDictionary ArgumentTypes)? GetParamsObjectDictionary(object? paramsObject)
+ {
+ if (paramsObject is null)
+ {
+ return default;
+ }
+
+ // Look up the argument types dictionary if we saved it before.
+ Type paramsObjectType = paramsObject.GetType();
+ IReadOnlyDictionary? argumentTypes;
+ lock (ParameterObjectPropertyTypes)
+ {
+ ParameterObjectPropertyTypes.TryGetValue(paramsObjectType, out argumentTypes);
+ }
+
+ // If we couldn't find a previously created argument types dictionary, create a mutable one that we'll build this time.
+ Dictionary? mutableArgumentTypes = argumentTypes is null ? [] : null;
+
+ var result = new Dictionary(StringComparer.Ordinal);
+
+ TypeInfo paramsTypeInfo = paramsObject.GetType().GetTypeInfo();
+ bool isDataContract = paramsTypeInfo.GetCustomAttribute() is not null;
+
+ BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance;
+ if (isDataContract)
+ {
+ bindingFlags |= BindingFlags.NonPublic;
+ }
+
+ bool TryGetSerializationInfo(MemberInfo memberInfo, out string key)
+ {
+ key = memberInfo.Name;
+ if (isDataContract)
+ {
+ DataMemberAttribute? dataMemberAttribute = memberInfo.GetCustomAttribute();
+ if (dataMemberAttribute is null)
+ {
+ return false;
+ }
+
+ if (!dataMemberAttribute.EmitDefaultValue)
+ {
+ throw new NotSupportedException($"(DataMemberAttribute.EmitDefaultValue == false) is not supported but was found on: {memberInfo.DeclaringType!.FullName}.{memberInfo.Name}.");
+ }
+
+ key = dataMemberAttribute.Name ?? memberInfo.Name;
+ return true;
+ }
+ else
+ {
+ return memberInfo.GetCustomAttribute() is null;
+ }
+ }
+
+ foreach (PropertyInfo property in paramsTypeInfo.GetProperties(bindingFlags))
+ {
+ if (property.GetMethod is not null)
+ {
+ if (TryGetSerializationInfo(property, out string key))
+ {
+ result[key] = property.GetValue(paramsObject);
+ if (mutableArgumentTypes is not null)
+ {
+ mutableArgumentTypes[key] = property.PropertyType;
+ }
+ }
+ }
+ }
+
+ foreach (FieldInfo field in paramsTypeInfo.GetFields(bindingFlags))
+ {
+ if (TryGetSerializationInfo(field, out string key))
+ {
+ result[key] = field.GetValue(paramsObject);
+ if (mutableArgumentTypes is not null)
+ {
+ mutableArgumentTypes[key] = field.FieldType;
+ }
+ }
+ }
+
+ // If we assembled the argument types dictionary this time, save it for next time.
+ if (mutableArgumentTypes is not null)
+ {
+ lock (ParameterObjectPropertyTypes)
+ {
+ if (ParameterObjectPropertyTypes.TryGetValue(paramsObjectType, out IReadOnlyDictionary? lostRace))
+ {
+ // Of the two, pick the winner to use ourselves so we consolidate on one and allow the GC to collect the loser sooner.
+ argumentTypes = lostRace;
+ }
+ else
+ {
+ ParameterObjectPropertyTypes.Add(paramsObjectType, argumentTypes = mutableArgumentTypes);
+ }
+ }
+ }
+
+ return (result, argumentTypes!);
+ }
+
+ ///
+ /// Reads a string with an optimized path for the value "2.0".
+ ///
+ /// The reader to use.
+ /// The decoded string.
+ private static string ReadProtocolVersion(ref MessagePackReader reader)
+ {
+ // Recognize "2.0" since we expect it and can avoid decoding and allocating a new string for it.
+ return Version2.TryRead(ref reader)
+ ? Version2.Value
+ : reader.ReadString() ?? throw new MessagePackSerializationException(Resources.RequiredArgumentMissing);
+ }
+
+ ///
+ /// Writes the JSON-RPC version property name and value in a highly optimized way.
+ ///
+ private static void WriteProtocolVersionPropertyAndValue(ref MessagePackWriter writer, string version)
+ {
+ writer.Write(VersionPropertyName);
+ writer.Write(version);
+ }
+
+ private static void ReadUnknownProperty(ref MessagePackReader reader, in SerializationContext context, ref Dictionary? topLevelProperties)
+ {
+ topLevelProperties ??= new Dictionary(StringComparer.Ordinal);
+ string name = context.GetConverter(Witness.ShapeProvider).Read(ref reader, context) ?? throw new MessagePackSerializationException("Unexpected nil at property name position.");
+ topLevelProperties.Add(name, reader.ReadRaw(context));
+ }
+
+ private static Type NormalizeType(Type type)
+ {
+ if (TrackerHelpers.FindIProgressInterfaceImplementedBy(type) is Type iface)
+ {
+ type = iface;
+ }
+ else if (TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(type) is Type iface2)
+ {
+ type = iface2;
+ }
+ else if (typeof(IDuplexPipe).IsAssignableFrom(type))
+ {
+ type = typeof(IDuplexPipe);
+ }
+ else if (typeof(PipeWriter).IsAssignableFrom(type))
+ {
+ type = typeof(PipeWriter);
+ }
+ else if (typeof(PipeReader).IsAssignableFrom(type))
+ {
+ type = typeof(PipeReader);
+ }
+ else if (typeof(Stream).IsAssignableFrom(type))
+ {
+ type = typeof(Stream);
+ }
+ else if (typeof(Exception).IsAssignableFrom(type))
+ {
+ type = typeof(Exception);
+ }
+
+ return type;
+ }
+
+ private static T ActivateAssociatedType(ITypeShape shape, Type associatedType)
+ where T : class
+ => (T?)((IObjectTypeShape?)shape.GetAssociatedTypeShape(associatedType))?.GetDefaultConstructor()?.Invoke() ?? throw new InvalidOperationException($"Missing associated type from {shape.Type.FullName} to {associatedType.FullName}.");
+
+ private ITypeShape GetUserDataShape(Type type)
+ {
+ type = NormalizeType(type);
+
+ // We prefer to get the shape from the user shape provider, but will fallback to our own for built-in types.
+ // But if that fails too, try again with Resolve on the user shape provider so that it throws an exception explaining that the user needs to provide it.
+ return this.TypeShapeProvider.GetShape(type) ?? Witness.ShapeProvider.GetShape(type) ?? this.TypeShapeProvider.Resolve(type);
+ }
+
+ private void WriteUserData(ref MessagePackWriter writer, object? value, Type? valueType, SerializationContext context)
+ {
+ if (value is null)
+ {
+ writer.WriteNil();
+ }
+ else
+ {
+ if (valueType == typeof(void) || valueType == typeof(object))
+ {
+ valueType = null;
+ }
+
+ ITypeShape valueShape = this.GetUserDataShape(valueType ?? value.GetType());
+ this.UserDataSerializer.SerializeObject(ref writer, value, valueShape, context.CancellationToken);
+ }
+ }
+
+ ///
+ /// Converts JSON-RPC messages to and from MessagePack format.
+ ///
+ internal class JsonRpcMessageConverter : MessagePackConverter
+ {
+ ///
+ /// Reads a JSON-RPC message from the specified MessagePack reader.
+ ///
+ /// The MessagePack reader to read from.
+ /// The serialization context.
+ /// The deserialized JSON-RPC message.
+ public override JsonRpcMessage? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ context.DepthStep();
+
+ MessagePackReader readAhead = reader.CreatePeekReader();
+ int propertyCount = readAhead.ReadMapHeader();
+
+ for (int i = 0; i < propertyCount; i++)
+ {
+ if (MethodPropertyName.TryRead(ref readAhead))
+ {
+ return context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else if (ResultPropertyName.TryRead(ref readAhead))
+ {
+ return context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else if (ErrorPropertyName.TryRead(ref readAhead))
+ {
+ return context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+
+ // This property doesn't tell us the message type.
+ // Skip its name and value.
+ readAhead.Skip(context);
+ readAhead.Skip(context);
+ }
+
+ throw new UnrecognizedJsonRpcMessageException();
+ }
+
+ ///
+ /// Writes a JSON-RPC message to the specified MessagePack writer.
+ ///
+ /// The MessagePack writer to write to.
+ /// The JSON-RPC message to write.
+ /// The serialization context.
+ public override void Write(ref MessagePackWriter writer, in JsonRpcMessage? value, SerializationContext context)
+ {
+ Requires.NotNull(value!, nameof(value));
+
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+ context.DepthStep();
+
+ using (formatter.TrackSerialization(value))
+ {
+ switch (value)
+ {
+ case Protocol.JsonRpcRequest request:
+ context.GetConverter(context.TypeShapeProvider).Write(ref writer, request, context);
+ break;
+ case Protocol.JsonRpcResult result:
+ context.GetConverter(context.TypeShapeProvider).Write(ref writer, result, context);
+ break;
+ case Protocol.JsonRpcError error:
+ context.GetConverter(context.TypeShapeProvider).Write(ref writer, error, context);
+ break;
+ default:
+ throw new NotSupportedException("Unexpected JsonRpcMessage-derived type: " + value.GetType().Name);
+ }
+ }
+ }
+
+ ///
+ /// Gets the JSON schema for the specified type.
+ ///
+ /// The JSON schema context.
+ /// The type shape.
+ /// The JSON schema for the specified type.
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+
+ ///
+ /// Converts a JSON-RPC request message to and from MessagePack format.
+ ///
+ internal class JsonRpcRequestConverter : MessagePackConverter
+ {
+ ///
+ /// Reads a JSON-RPC request message from the specified MessagePack reader.
+ ///
+ /// The MessagePack reader to read from.
+ /// The serialization context.
+ /// The deserialized JSON-RPC request message.
+ public override Protocol.JsonRpcRequest? Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ var result = new JsonRpcRequest(formatter)
+ {
+ OriginalMessagePack = (RawMessagePack)reader.Sequence,
+ };
+
+ Dictionary? topLevelProperties = null;
+
+ int propertyCount = reader.ReadMapHeader();
+ for (int propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++)
+ {
+ if (VersionPropertyName.TryRead(ref reader))
+ {
+ result.Version = ReadProtocolVersion(ref reader);
+ }
+ else if (IdPropertyName.TryRead(ref reader))
+ {
+ result.RequestId = context.GetConverter(null).Read(ref reader, context);
+ }
+ else if (MethodPropertyName.TryRead(ref reader))
+ {
+ result.Method = context.GetConverter(Witness.ShapeProvider).Read(ref reader, context);
+ }
+ else if (ParamsPropertyName.TryRead(ref reader))
+ {
+ SequencePosition paramsTokenStartPosition = reader.Position;
+
+ // Parse out the arguments into a dictionary or array, but don't deserialize them because we don't yet know what types to deserialize them to.
+ switch (reader.NextMessagePackType)
+ {
+ case MessagePackType.Array:
+ var positionalArgs = new RawMessagePack[reader.ReadArrayHeader()];
+ for (int i = 0; i < positionalArgs.Length; i++)
+ {
+ positionalArgs[i] = reader.ReadRaw(context);
+ }
+
+ result.MsgPackPositionalArguments = positionalArgs;
+ break;
+ case MessagePackType.Map:
+ int namedArgsCount = reader.ReadMapHeader();
+ var namedArgs = new Dictionary(namedArgsCount, StringComparer.Ordinal);
+ for (int i = 0; i < namedArgsCount; i++)
+ {
+ // Use a string converter so that strings can be interned.
+ string propertyName = context.GetConverter(Witness.ShapeProvider).Read(ref reader, context) ?? throw new MessagePackSerializationException(Resources.UnexpectedNullValueInMap);
+ namedArgs.Add(propertyName, reader.ReadRaw(context));
+ }
+
+ result.MsgPackNamedArguments = namedArgs;
+ break;
+ case MessagePackType.Nil:
+ result.MsgPackPositionalArguments = [];
+ reader.ReadNil();
+ break;
+ case MessagePackType type:
+ throw new MessagePackSerializationException("Expected a map or array of arguments but got " + type);
+ }
+
+ result.MsgPackArguments = (RawMessagePack)reader.Sequence.Slice(paramsTokenStartPosition, reader.Position);
+ }
+ else if (TraceParentPropertyName.TryRead(ref reader))
+ {
+ TraceParent traceParent = context.GetConverter(null).Read(ref reader, context);
+ result.TraceParent = traceParent.ToString();
+ }
+ else if (TraceStatePropertyName.TryRead(ref reader))
+ {
+ result.TraceState = ReadTraceState(ref reader, context);
+ }
+ else
+ {
+ ReadUnknownProperty(ref reader, context, ref topLevelProperties);
+ }
+ }
+
+ if (topLevelProperties is not null)
+ {
+ result.TopLevelPropertyBag = new TopLevelPropertyBag(formatter, topLevelProperties);
+ }
+
+ formatter.TryHandleSpecialIncomingMessage(result);
+
+ return result;
+ }
+
+ ///
+ /// Writes a JSON-RPC request message to the specified MessagePack writer.
+ ///
+ /// The MessagePack writer to write to.
+ /// The JSON-RPC request message to write.
+ /// The serialization context.
+ public override void Write(ref MessagePackWriter writer, in Protocol.JsonRpcRequest? value, SerializationContext context)
+ {
+ Requires.NotNull(value!, nameof(value));
+
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ var topLevelPropertyBag = (TopLevelPropertyBag?)(value as IMessageWithTopLevelPropertyBag)?.TopLevelPropertyBag;
+
+ int mapElementCount = value.RequestId.IsEmpty ? 3 : 4;
+ if (value.TraceParent?.Length > 0)
+ {
+ mapElementCount++;
+ if (value.TraceState?.Length > 0)
+ {
+ mapElementCount++;
+ }
+ }
+
+ mapElementCount += topLevelPropertyBag?.PropertyCount ?? 0;
+ writer.WriteMapHeader(mapElementCount);
+
+ WriteProtocolVersionPropertyAndValue(ref writer, value.Version);
+
+ if (!value.RequestId.IsEmpty)
+ {
+ writer.Write(IdPropertyName);
+ context.GetConverter(Witness.ShapeProvider).Write(ref writer, value.RequestId, context);
+ }
+
+ writer.Write(MethodPropertyName);
+ writer.Write(value.Method);
+
+ writer.Write(ParamsPropertyName);
+ if (value.ArgumentsList is not null)
+ {
+ writer.WriteArrayHeader(value.ArgumentsList.Count);
+
+ for (int i = 0; i < value.ArgumentsList.Count; i++)
+ {
+ formatter.WriteUserData(ref writer, value.ArgumentsList[i], value.ArgumentListDeclaredTypes?[i], context);
+ }
+ }
+ else if (value.NamedArguments is not null)
+ {
+ writer.WriteMapHeader(value.NamedArguments.Count);
+ foreach (KeyValuePair entry in value.NamedArguments)
+ {
+ writer.Write(entry.Key);
+ formatter.WriteUserData(ref writer, entry.Value, value.NamedArgumentDeclaredTypes?[entry.Key], context);
+ }
+ }
+ else
+ {
+ writer.WriteNil();
+ }
+
+ if (value.TraceParent?.Length > 0)
+ {
+ writer.Write(TraceParentPropertyName);
+ context.GetConverter(Witness.ShapeProvider).Write(ref writer, new TraceParent(value.TraceParent), context);
+
+ if (value.TraceState?.Length > 0)
+ {
+ writer.Write(TraceStatePropertyName);
+ WriteTraceState(ref writer, value.TraceState);
+ }
+ }
+
+ topLevelPropertyBag?.WriteProperties(ref writer);
+ }
+
+ ///
+ /// Gets the JSON schema for the specified type.
+ ///
+ /// The JSON schema context.
+ /// The type shape.
+ /// The JSON schema for the specified type.
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+
+ private static void WriteTraceState(ref MessagePackWriter writer, string traceState)
+ {
+ ReadOnlySpan traceStateChars = traceState.AsSpan();
+
+ // Count elements first so we can write the header.
+ int elementCount = 1;
+ int commaIndex;
+ while ((commaIndex = traceStateChars.IndexOf(',')) >= 0)
+ {
+ elementCount++;
+ traceStateChars = traceStateChars.Slice(commaIndex + 1);
+ }
+
+ // For every element, we have a key and value to record.
+ writer.WriteArrayHeader(elementCount * 2);
+
+ traceStateChars = traceState.AsSpan();
+ while ((commaIndex = traceStateChars.IndexOf(',')) >= 0)
+ {
+ ReadOnlySpan element = traceStateChars.Slice(0, commaIndex);
+ WritePair(ref writer, element);
+ traceStateChars = traceStateChars.Slice(commaIndex + 1);
+ }
+
+ // Write out the last one.
+ WritePair(ref writer, traceStateChars);
+
+ static void WritePair(ref MessagePackWriter writer, ReadOnlySpan pair)
+ {
+ int equalsIndex = pair.IndexOf('=');
+ ReadOnlySpan key = pair.Slice(0, equalsIndex);
+ ReadOnlySpan value = pair.Slice(equalsIndex + 1);
+ writer.Write(key);
+ writer.Write(value);
+ }
+ }
+
+ private static unsafe string ReadTraceState(ref MessagePackReader reader, SerializationContext context)
+ {
+ int elements = reader.ReadArrayHeader();
+ if (elements % 2 != 0)
+ {
+ throw new NotSupportedException("Odd number of elements not expected.");
+ }
+
+ // With care, we could probably assemble this string with just two allocations (the string + a char[]).
+ var resultBuilder = new StringBuilder();
+ for (int i = 0; i < elements; i += 2)
+ {
+ if (resultBuilder.Length > 0)
+ {
+ resultBuilder.Append(',');
+ }
+
+ // We assume the key is a frequent string, and the value is unique,
+ // so we optimize whether to use string interning or not on that basis.
+ resultBuilder.Append(context.GetConverter(Witness.ShapeProvider).Read(ref reader, context));
+ resultBuilder.Append('=');
+ resultBuilder.Append(reader.ReadString());
+ }
+
+ return resultBuilder.ToString();
+ }
+ }
+
+ ///
+ /// Converts a JSON-RPC result message to and from MessagePack format.
+ ///
+ internal class JsonRpcResultConverter : MessagePackConverter
+ {
+ ///
+ /// Reads a JSON-RPC result message from the specified MessagePack reader.
+ ///
+ /// The MessagePack reader to read from.
+ /// The serialization context.
+ /// The deserialized JSON-RPC result message.
+ public override Protocol.JsonRpcResult Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+ context.DepthStep();
+
+ var result = new JsonRpcResult(formatter)
+ {
+ OriginalMessagePack = (RawMessagePack)reader.Sequence,
+ };
+
+ Dictionary? topLevelProperties = null;
+
+ int propertyCount = reader.ReadMapHeader();
+ for (int propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++)
+ {
+ if (VersionPropertyName.TryRead(ref reader))
+ {
+ result.Version = ReadProtocolVersion(ref reader);
+ }
+ else if (IdPropertyName.TryRead(ref reader))
+ {
+ result.RequestId = context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else if (ResultPropertyName.TryRead(ref reader))
+ {
+ result.MsgPackResult = reader.ReadRaw(context);
+ }
+ else
+ {
+ ReadUnknownProperty(ref reader, context, ref topLevelProperties);
+ }
+ }
+
+ if (topLevelProperties is not null)
+ {
+ result.TopLevelPropertyBag = new TopLevelPropertyBag(formatter, topLevelProperties);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Writes a JSON-RPC result message to the specified MessagePack writer.
+ ///
+ /// The MessagePack writer to write to.
+ /// The JSON-RPC result message to write.
+ /// The serialization context.
+ public override void Write(ref MessagePackWriter writer, in Protocol.JsonRpcResult? value, SerializationContext context)
+ {
+ Requires.NotNull(value!, nameof(value));
+
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+
+ context.DepthStep();
+
+ var topLevelPropertyBagMessage = value as IMessageWithTopLevelPropertyBag;
+
+ int mapElementCount = 3;
+ mapElementCount += (topLevelPropertyBagMessage?.TopLevelPropertyBag as TopLevelPropertyBag)?.PropertyCount ?? 0;
+ writer.WriteMapHeader(mapElementCount);
+
+ WriteProtocolVersionPropertyAndValue(ref writer, value.Version);
+
+ writer.Write(IdPropertyName);
+ context.GetConverter(context.TypeShapeProvider).Write(ref writer, value.RequestId, context);
+
+ writer.Write(ResultPropertyName);
+
+ if (value.Result is null)
+ {
+ writer.WriteNil();
+ }
+ else
+ {
+ formatter.WriteUserData(ref writer, value.Result, value.ResultDeclaredType, context);
+ }
+
+ (topLevelPropertyBagMessage?.TopLevelPropertyBag as TopLevelPropertyBag)?.WriteProperties(ref writer);
+ }
+
+ ///
+ /// Gets the JSON schema for the specified type.
+ ///
+ /// The JSON schema context.
+ /// The type shape.
+ /// The JSON schema for the specified type.
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+
+ ///
+ /// Converts a JSON-RPC error message to and from MessagePack format.
+ ///
+ internal class JsonRpcErrorConverter : MessagePackConverter
+ {
+ ///
+ /// Reads a JSON-RPC error message from the specified MessagePack reader.
+ ///
+ /// The MessagePack reader to read from.
+ /// The serialization context.
+ /// The deserialized JSON-RPC error message.
+ public override Protocol.JsonRpcError Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+ var error = new JsonRpcError(formatter)
+ {
+ OriginalMessagePack = (RawMessagePack)reader.Sequence,
+ };
+
+ Dictionary? topLevelProperties = null;
+
+ context.DepthStep();
+ int propertyCount = reader.ReadMapHeader();
+ for (int propertyIdx = 0; propertyIdx < propertyCount; propertyIdx++)
+ {
+ if (VersionPropertyName.TryRead(ref reader))
+ {
+ error.Version = ReadProtocolVersion(ref reader);
+ }
+ else if (IdPropertyName.TryRead(ref reader))
+ {
+ error.RequestId = context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else if (ErrorPropertyName.TryRead(ref reader))
+ {
+ error.Error = context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else
+ {
+ ReadUnknownProperty(ref reader, context, ref topLevelProperties);
+ }
+ }
+
+ if (topLevelProperties is not null)
+ {
+ error.TopLevelPropertyBag = new TopLevelPropertyBag(formatter, topLevelProperties);
+ }
+
+ return error;
+ }
+
+ ///
+ /// Writes a JSON-RPC error message to the specified MessagePack writer.
+ ///
+ /// The MessagePack writer to write to.
+ /// The JSON-RPC error message to write.
+ /// The serialization context.
+ public override void Write(ref MessagePackWriter writer, in Protocol.JsonRpcError? value, SerializationContext context)
+ {
+ Requires.NotNull(value!, nameof(value));
+
+ var topLevelPropertyBag = (TopLevelPropertyBag?)(value as IMessageWithTopLevelPropertyBag)?.TopLevelPropertyBag;
+
+ context.DepthStep();
+ int mapElementCount = 3;
+ mapElementCount += topLevelPropertyBag?.PropertyCount ?? 0;
+ writer.WriteMapHeader(mapElementCount);
+
+ WriteProtocolVersionPropertyAndValue(ref writer, value.Version);
+
+ writer.Write(IdPropertyName);
+ context.GetConverter(context.TypeShapeProvider)
+ .Write(ref writer, value.RequestId, context);
+
+ writer.Write(ErrorPropertyName);
+ context.GetConverter(context.TypeShapeProvider)
+ .Write(ref writer, value.Error, context);
+
+ topLevelPropertyBag?.WriteProperties(ref writer);
+ }
+
+ ///
+ /// Gets the JSON schema for the specified type.
+ ///
+ /// The JSON schema context.
+ /// The type shape.
+ /// The JSON schema for the specified type.
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+
+ ///
+ /// Converts a JSON-RPC error detail to and from MessagePack format.
+ ///
+ internal class JsonRpcErrorDetailConverter : MessagePackConverter
+ {
+ private static readonly MessagePackString CodePropertyName = new("code");
+ private static readonly MessagePackString MessagePropertyName = new("message");
+ private static readonly MessagePackString DataPropertyName = new("data");
+
+ ///
+ /// Reads a JSON-RPC error detail from the specified MessagePack reader.
+ ///
+ /// The MessagePack reader to read from.
+ /// The serialization context.
+ /// The deserialized JSON-RPC error detail.
+ public override Protocol.JsonRpcError.ErrorDetail Read(ref MessagePackReader reader, SerializationContext context)
+ {
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+ context.DepthStep();
+
+ var result = new JsonRpcError.ErrorDetail(formatter);
+
+ int propertyCount = reader.ReadMapHeader();
+ for (int propertyIdx = 0; propertyIdx < propertyCount; propertyIdx++)
+ {
+ if (CodePropertyName.TryRead(ref reader))
+ {
+ result.Code = context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else if (MessagePropertyName.TryRead(ref reader))
+ {
+ result.Message = context.GetConverter(context.TypeShapeProvider).Read(ref reader, context);
+ }
+ else if (DataPropertyName.TryRead(ref reader))
+ {
+ result.MsgPackData = reader.ReadRaw(context);
+ }
+ else
+ {
+ reader.Skip(context); // skip the key
+ reader.Skip(context); // skip the value
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Writes a JSON-RPC error detail to the specified MessagePack writer.
+ ///
+ /// The MessagePack writer to write to.
+ /// The JSON-RPC error detail to write.
+ /// The serialization context.
+ [SuppressMessage("Usage", "NBMsgPack031:Converters should read or write exactly one msgpack structure", Justification = "Writer is passed to user data context")]
+ public override void Write(ref MessagePackWriter writer, in Protocol.JsonRpcError.ErrorDetail? value, SerializationContext context)
+ {
+ Requires.NotNull(value!, nameof(value));
+
+ NerdbankMessagePackFormatter formatter = context.GetFormatter();
+ context.DepthStep();
+
+ writer.WriteMapHeader(3);
+
+ writer.Write(CodePropertyName);
+ context.GetConverter(context.TypeShapeProvider)
+ .Write(ref writer, value.Code, context);
+
+ writer.Write(MessagePropertyName);
+ writer.Write(value.Message);
+
+ writer.Write(DataPropertyName);
+ if (value.Data is null)
+ {
+ writer.WriteNil();
+ }
+ else
+ {
+ // We generally leave error data for the user to provide the shape for.
+ // But for CommonErrorData, we can take responsibility for that.
+ // We also take responsibility for Exception serialization (for now).
+ ITypeShapeProvider provider = value.Data is CommonErrorData or Exception ? Witness.ShapeProvider : formatter.TypeShapeProvider;
+ Type declaredType = value.Data is Exception ? typeof(Exception) : value.Data.GetType();
+ context.GetConverter(declaredType, provider).WriteObject(ref writer, value.Data, context);
+ }
+ }
+
+ ///
+ /// Gets the JSON schema for the specified type.
+ ///
+ /// The JSON schema context.
+ /// The type shape.
+ /// The JSON schema for the specified type.
+ public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
+ }
+
+ private class TopLevelPropertyBag : TopLevelPropertyBagBase
+ {
+ private readonly NerdbankMessagePackFormatter formatter;
+ private readonly IReadOnlyDictionary? inboundUnknownProperties;
+
+ ///
+ /// Initializes a new instance of the class
+ /// for an incoming message.
+ ///
+ /// The owning formatter.
+ /// The map of unrecognized inbound properties.
+ internal TopLevelPropertyBag(NerdbankMessagePackFormatter formatter, IReadOnlyDictionary inboundUnknownProperties)
+ : base(isOutbound: false)
+ {
+ this.formatter = formatter;
+ this.inboundUnknownProperties = inboundUnknownProperties;
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// for an outbound message.
+ ///
+ /// The owning formatter.
+ internal TopLevelPropertyBag(NerdbankMessagePackFormatter formatter)
+ : base(isOutbound: true)
+ {
+ this.formatter = formatter;
+ }
+
+ 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 WriteProperties(ref MessagePackWriter writer)
+ {
+ if (this.inboundUnknownProperties 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 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.inboundUnknownProperties)
+ ////{
+ //// writer.Write(entry.Key);
+ //// writer.Write(entry.Value);
+ ////}
+ }
+ else
+ {
+ foreach (KeyValuePair entry in this.OutboundProperties)
+ {
+ writer.Write(entry.Key);
+ this.formatter.userDataSerializer.SerializeObject(ref writer, entry.Value.Value, this.formatter.TypeShapeProvider.Resolve(entry.Value.DeclaredType));
+ }
+ }
+ }
+
+ protected internal override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value)
+ {
+ if (this.inboundUnknownProperties is null)
+ {
+ throw new InvalidOperationException(Resources.InboundMessageOnly);
+ }
+
+ value = default;
+
+ if (this.inboundUnknownProperties.TryGetValue(name, out RawMessagePack serializedValue) is true)
+ {
+ value = this.formatter.userDataSerializer.Deserialize(serializedValue, this.formatter.TypeShapeProvider);
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ private class OutboundJsonRpcRequest(NerdbankMessagePackFormatter formatter) : JsonRpcRequestBase
+ {
+ protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(formatter);
+ }
+
+ [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ private class JsonRpcRequest : JsonRpcRequestBase, IJsonRpcMessagePackRetention
+ {
+ private readonly NerdbankMessagePackFormatter formatter;
+
+ internal JsonRpcRequest(NerdbankMessagePackFormatter formatter)
+ {
+ this.formatter = formatter;
+ }
+
+ public override int ArgumentCount => this.MsgPackNamedArguments?.Count ?? this.MsgPackPositionalArguments?.Count ?? base.ArgumentCount;
+
+ public override IEnumerable? ArgumentNames => this.MsgPackNamedArguments?.Keys;
+
+ public RawMessagePack OriginalMessagePack { get; internal set; }
+
+ internal RawMessagePack MsgPackArguments { get; set; }
+
+ internal IReadOnlyDictionary? MsgPackNamedArguments { get; set; }
+
+ internal IReadOnlyList? MsgPackPositionalArguments { get; set; }
+
+ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span
[DataContract]
-public class CommonErrorData
+[GenerateShape]
+public partial class CommonErrorData
{
///
/// Initializes a new instance of the class.
@@ -39,6 +42,7 @@ public CommonErrorData(Exception copyFrom)
///
[DataMember(Order = 0, Name = "type")]
[STJ.JsonPropertyName("type"), STJ.JsonPropertyOrder(0)]
+ [PropertyShape(Name = "type"), NBMsgPack.Key(0)]
public string? TypeName { get; set; }
///
@@ -46,6 +50,7 @@ public CommonErrorData(Exception copyFrom)
///
[DataMember(Order = 1, Name = "message")]
[STJ.JsonPropertyName("message"), STJ.JsonPropertyOrder(1)]
+ [PropertyShape(Name = "message"), NBMsgPack.Key(1)]
public string? Message { get; set; }
///
@@ -53,6 +58,7 @@ public CommonErrorData(Exception copyFrom)
///
[DataMember(Order = 2, Name = "stack")]
[STJ.JsonPropertyName("stack"), STJ.JsonPropertyOrder(2)]
+ [PropertyShape(Name = "stack"), NBMsgPack.Key(2)]
public string? StackTrace { get; set; }
///
@@ -60,6 +66,7 @@ public CommonErrorData(Exception copyFrom)
///
[DataMember(Order = 3, Name = "code")]
[STJ.JsonPropertyName("code"), STJ.JsonPropertyOrder(3)]
+ [PropertyShape(Name = "code"), NBMsgPack.Key(3)]
public int HResult { get; set; }
///
@@ -67,5 +74,6 @@ public CommonErrorData(Exception copyFrom)
///
[DataMember(Order = 4, Name = "inner")]
[STJ.JsonPropertyName("inner"), STJ.JsonPropertyOrder(4)]
+ [PropertyShape(Name = "inner"), NBMsgPack.Key(4)]
public CommonErrorData? Inner { get; set; }
}
diff --git a/src/StreamJsonRpc/Protocol/JsonRpcError.cs b/src/StreamJsonRpc/Protocol/JsonRpcError.cs
index b673edcb..38ccfa91 100644
--- a/src/StreamJsonRpc/Protocol/JsonRpcError.cs
+++ b/src/StreamJsonRpc/Protocol/JsonRpcError.cs
@@ -4,6 +4,8 @@
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text.Json.Nodes;
+using Nerdbank.MessagePack;
+using PolyType;
using StreamJsonRpc.Reflection;
using STJ = System.Text.Json.Serialization;
@@ -13,14 +15,17 @@ namespace StreamJsonRpc.Protocol;
/// Describes the error resulting from a that failed on the server.
///
[DataContract]
+[GenerateShape]
+[MessagePackConverter(typeof(NerdbankMessagePackFormatter.JsonRpcErrorConverter))]
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")]
-public class JsonRpcError : JsonRpcMessage, IJsonRpcMessageWithId
+public partial class JsonRpcError : JsonRpcMessage, IJsonRpcMessageWithId
{
///
/// Gets or sets the detail about the error.
///
[DataMember(Name = "error", Order = 2, IsRequired = true)]
[STJ.JsonPropertyName("error"), STJ.JsonPropertyOrder(2), STJ.JsonRequired]
+ [PropertyShape(Name = "error", Order = 2)]
public ErrorDetail? Error { get; set; }
///
@@ -30,6 +35,7 @@ public class JsonRpcError : JsonRpcMessage, IJsonRpcMessageWithId
[Obsolete("Use " + nameof(RequestId) + " instead.")]
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public object? Id
{
get => this.RequestId.ObjectValue;
@@ -41,6 +47,7 @@ public object? Id
///
[DataMember(Name = "id", Order = 1, IsRequired = true, EmitDefaultValue = true)]
[STJ.JsonPropertyName("id"), STJ.JsonPropertyOrder(1), STJ.JsonRequired]
+ [PropertyShape(Name = "id", Order = 1)]
public RequestId RequestId { get; set; }
///
@@ -66,7 +73,9 @@ public override string ToString()
/// Describes the error.
///
[DataContract]
- public class ErrorDetail
+ [GenerateShape]
+ [MessagePackConverter(typeof(NerdbankMessagePackFormatter.JsonRpcErrorDetailConverter))]
+ public partial class ErrorDetail
{
///
/// Gets or sets a number that indicates the error type that occurred.
@@ -77,6 +86,7 @@ public class ErrorDetail
///
[DataMember(Name = "code", Order = 0, IsRequired = true)]
[STJ.JsonPropertyName("code"), STJ.JsonPropertyOrder(0), STJ.JsonRequired]
+ [PropertyShape(Name = "code", Order = 0)]
public JsonRpcErrorCode Code { get; set; }
///
@@ -87,6 +97,7 @@ public class ErrorDetail
///
[DataMember(Name = "message", Order = 1, IsRequired = true)]
[STJ.JsonPropertyName("message"), STJ.JsonPropertyOrder(1), STJ.JsonRequired]
+ [PropertyShape(Name = "message", Order = 1)]
public string? Message { get; set; }
///
@@ -95,6 +106,7 @@ public class ErrorDetail
[DataMember(Name = "data", Order = 2, IsRequired = false)]
[Newtonsoft.Json.JsonProperty(DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore)]
[STJ.JsonPropertyName("data"), STJ.JsonPropertyOrder(2)]
+ [PropertyShape(Name = "data", Order = 2)]
public object? Data { get; set; }
///
@@ -129,7 +141,7 @@ public class ErrorDetail
///
/// The type that will be used as the generic type argument to .
///
- /// Overridding methods in types that retain buffers used to deserialize should deserialize within this method and clear those buffers
+ /// Overriding methods in types that retain buffers used to deserialize should deserialize within this method and clear those buffers
/// to prevent further access to these buffers which may otherwise happen concurrently with a call to
/// that would recycle the same buffer being deserialized from.
///
diff --git a/src/StreamJsonRpc/Protocol/JsonRpcMessage.cs b/src/StreamJsonRpc/Protocol/JsonRpcMessage.cs
index 84acc937..d9487634 100644
--- a/src/StreamJsonRpc/Protocol/JsonRpcMessage.cs
+++ b/src/StreamJsonRpc/Protocol/JsonRpcMessage.cs
@@ -3,6 +3,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
+using Nerdbank.MessagePack;
+using PolyType;
using STJ = System.Text.Json.Serialization;
namespace StreamJsonRpc.Protocol;
@@ -14,7 +16,9 @@ namespace StreamJsonRpc.Protocol;
[KnownType(typeof(JsonRpcRequest))]
[KnownType(typeof(JsonRpcResult))]
[KnownType(typeof(JsonRpcError))]
-public abstract class JsonRpcMessage
+[MessagePackConverter(typeof(NerdbankMessagePackFormatter.JsonRpcMessageConverter))]
+[GenerateShape]
+public abstract partial class JsonRpcMessage
{
///
/// Gets or sets the version of the JSON-RPC protocol that this message conforms to.
@@ -22,6 +26,7 @@ public abstract class JsonRpcMessage
/// Defaults to "2.0".
[DataMember(Name = "jsonrpc", Order = 0, IsRequired = true)]
[STJ.JsonPropertyName("jsonrpc"), STJ.JsonPropertyOrder(0), STJ.JsonRequired]
+ [PropertyShape(Name = "jsonrpc", Order = 0)]
public string Version { get; set; } = "2.0";
///
diff --git a/src/StreamJsonRpc/Protocol/JsonRpcRequest.cs b/src/StreamJsonRpc/Protocol/JsonRpcRequest.cs
index 904b21d2..185a1c84 100644
--- a/src/StreamJsonRpc/Protocol/JsonRpcRequest.cs
+++ b/src/StreamJsonRpc/Protocol/JsonRpcRequest.cs
@@ -5,6 +5,9 @@
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json.Nodes;
+using Nerdbank.MessagePack;
+using PolyType;
+using JsonNET = Newtonsoft.Json.Linq;
using STJ = System.Text.Json.Serialization;
namespace StreamJsonRpc.Protocol;
@@ -13,8 +16,10 @@ namespace StreamJsonRpc.Protocol;
/// Describes a method to be invoked on the server.
///
[DataContract]
+[GenerateShape]
+[MessagePackConverter(typeof(NerdbankMessagePackFormatter.JsonRpcRequestConverter))]
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
-public class JsonRpcRequest : JsonRpcMessage, IJsonRpcMessageWithId
+public partial class JsonRpcRequest : JsonRpcMessage, IJsonRpcMessageWithId
{
///
/// The result of an attempt to match request arguments with a candidate method's parameters.
@@ -47,6 +52,7 @@ public enum ArgumentMatchResult
///
[DataMember(Name = "method", Order = 2, IsRequired = true)]
[STJ.JsonPropertyName("method"), STJ.JsonPropertyOrder(2), STJ.JsonRequired]
+ [PropertyShape(Name = "method", Order = 2)]
public string? Method { get; set; }
///
@@ -61,6 +67,7 @@ public enum ArgumentMatchResult
///
[DataMember(Name = "params", Order = 3, IsRequired = false, EmitDefaultValue = false)]
[STJ.JsonPropertyName("params"), STJ.JsonPropertyOrder(3), STJ.JsonIgnore(Condition = STJ.JsonIgnoreCondition.WhenWritingNull)]
+ [PropertyShape(Name = "params", Order = 3)]
public object? Arguments { get; set; }
///
@@ -70,6 +77,7 @@ public enum ArgumentMatchResult
[Obsolete("Use " + nameof(RequestId) + " instead.")]
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public object? Id
{
get => this.RequestId.ObjectValue;
@@ -81,6 +89,7 @@ public object? Id
///
[DataMember(Name = "id", Order = 1, IsRequired = false, EmitDefaultValue = false)]
[STJ.JsonPropertyName("id"), STJ.JsonPropertyOrder(1), STJ.JsonIgnore(Condition = STJ.JsonIgnoreCondition.WhenWritingDefault)]
+ [PropertyShape(Name = "id", Order = 1)]
public RequestId RequestId { get; set; }
///
@@ -88,6 +97,7 @@ public object? Id
///
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public bool IsResponseExpected => !this.RequestId.IsEmpty;
///
@@ -95,6 +105,7 @@ public object? Id
///
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public bool IsNotification => this.RequestId.IsEmpty;
///
@@ -102,6 +113,7 @@ public object? Id
///
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public virtual int ArgumentCount => this.NamedArguments?.Count ?? this.ArgumentsList?.Count ?? 0;
///
@@ -109,6 +121,7 @@ public object? Id
///
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public IReadOnlyDictionary? NamedArguments
{
get => this.Arguments as IReadOnlyDictionary;
@@ -127,6 +140,7 @@ public object? Id
///
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public IReadOnlyDictionary? NamedArgumentDeclaredTypes { get; set; }
///
@@ -134,6 +148,7 @@ public object? Id
///
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
[Obsolete("Use " + nameof(ArgumentsList) + " instead.")]
public object?[]? ArgumentsArray
{
@@ -146,6 +161,7 @@ public object?[]? ArgumentsArray
///
[IgnoreDataMember]
[STJ.JsonIgnore]
+ [PropertyShape(Ignore = true)]
public IReadOnlyList