diff --git a/Directory.Packages.props b/Directory.Packages.props
index 462cbe578..6a87cb5f6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,11 +22,12 @@
+
-
+
-
+
diff --git a/nuget.config b/nuget.config
index 0a73357e8..8ef7b7880 100644
--- a/nuget.config
+++ b/nuget.config
@@ -5,10 +5,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs
index 7dd4c6479..c7bdb6ffa 100644
--- a/src/StreamJsonRpc/FormatterBase.cs
+++ b/src/StreamJsonRpc/FormatterBase.cs
@@ -6,6 +6,7 @@
using System.IO.Pipelines;
using System.Reflection;
using System.Runtime.Serialization;
+using Nerdbank.MessagePack;
using Nerdbank.Streams;
using StreamJsonRpc.Protocol;
using StreamJsonRpc.Reflection;
diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs
index e8ab2712a..e5f1e596a 100644
--- a/src/StreamJsonRpc/JsonRpc.cs
+++ b/src/StreamJsonRpc/JsonRpc.cs
@@ -1697,7 +1697,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.
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.CommonString.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.CommonString.cs
new file mode 100644
index 000000000..dfc6ef831
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.CommonString.cs
@@ -0,0 +1,104 @@
+// 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 NBMP = Nerdbank.MessagePack;
+
+namespace StreamJsonRpc;
+
+public partial class NerdbankMessagePackFormatter
+{
+ [DebuggerDisplay("{" + nameof(Value) + "}")]
+ private struct CommonString
+ {
+ internal CommonString(string value)
+ {
+ Requires.Argument(value.Length > 0 && value.Length <= 16, nameof(value), "Length must be >0 and <=16.");
+ this.Value = value;
+ ReadOnlyMemory encodedBytes = MessagePack.Internal.CodeGenHelpers.GetEncodedStringBytes(value);
+ this.EncodedBytes = encodedBytes;
+
+ ReadOnlySpan span = this.EncodedBytes.Span.Slice(1);
+ this.Key = MessagePack.Internal.AutomataKeyGen.GetKey(ref span); // header is 1 byte because string length <= 16
+ this.Key2 = span.Length > 0 ? (ulong?)MessagePack.Internal.AutomataKeyGen.GetKey(ref span) : null;
+ }
+
+ ///
+ /// Gets the original string.
+ ///
+ internal string Value { get; }
+
+ ///
+ /// Gets the 64-bit integer that represents the string without decoding it.
+ ///
+ private ulong Key { get; }
+
+ ///
+ /// Gets the next 64-bit integer that represents the string without decoding it.
+ ///
+ private ulong? Key2 { get; }
+
+ ///
+ /// Gets the messagepack header and UTF-8 bytes for this string.
+ ///
+ private ReadOnlyMemory EncodedBytes { get; }
+
+ ///
+ /// Writes out the messagepack binary for this common string, if it matches the given value.
+ ///
+ /// The writer to use.
+ /// The value to be written, if it matches this .
+ /// if matches this and it was written; otherwise.
+ internal bool TryWrite(ref NBMP::MessagePackWriter writer, string value)
+ {
+ if (value == this.Value)
+ {
+ this.Write(ref writer);
+ return true;
+ }
+
+ return false;
+ }
+
+ internal readonly void Write(ref NBMP::MessagePackWriter writer) => writer.WriteRaw(this.EncodedBytes.Span);
+
+ ///
+ /// Checks whether a span of UTF-8 bytes equal this common string.
+ ///
+ /// The UTF-8 string.
+ /// if the UTF-8 bytes are the encoding of this common string; otherwise.
+ internal readonly bool TryRead(ReadOnlySpan utf8String)
+ {
+ if (utf8String.Length != this.EncodedBytes.Length - 1)
+ {
+ return false;
+ }
+
+ ulong key1 = MessagePack.Internal.AutomataKeyGen.GetKey(ref utf8String);
+ if (key1 != this.Key)
+ {
+ return false;
+ }
+
+ if (utf8String.Length > 0)
+ {
+ if (!this.Key2.HasValue)
+ {
+ return false;
+ }
+
+ ulong key2 = MessagePack.Internal.AutomataKeyGen.GetKey(ref utf8String);
+ if (key2 != this.Key2.Value)
+ {
+ return false;
+ }
+ }
+ else if (this.Key2.HasValue)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.FormatterContext.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.FormatterContext.cs
new file mode 100644
index 000000000..3d560bbc5
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.FormatterContext.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Nerdbank.MessagePack;
+using PolyType;
+using PolyType.Abstractions;
+
+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.
+///
+public sealed partial class NerdbankMessagePackFormatter
+{
+ internal class FormatterContext(MessagePackSerializer serializer, ITypeShapeProvider shapeProvider)
+ {
+ public MessagePackSerializer Serializer => serializer;
+
+ public ITypeShapeProvider ShapeProvider => shapeProvider;
+
+ public T? Deserialize(ref MessagePackReader reader, CancellationToken cancellationToken = default)
+ {
+ return serializer.Deserialize(ref reader, shapeProvider, cancellationToken);
+ }
+
+ public T Deserialize(in RawMessagePack pack, CancellationToken cancellationToken = default)
+ {
+ // TODO: Improve the exception
+ return serializer.Deserialize(pack, shapeProvider, cancellationToken)
+ ?? throw new InvalidOperationException("Deserialization failed.");
+ }
+
+ public object? DeserializeObject(in RawMessagePack pack, Type objectType, CancellationToken cancellationToken = default)
+ {
+ MessagePackReader reader = new(pack);
+ return serializer.DeserializeObject(
+ ref reader,
+ shapeProvider.Resolve(objectType),
+ cancellationToken);
+ }
+
+ public void Serialize(ref MessagePackWriter writer, T? value, CancellationToken cancellationToken = default)
+ {
+ serializer.Serialize(ref writer, value, shapeProvider, cancellationToken);
+ }
+
+ internal void SerializeObject(ref MessagePackWriter writer, object? value, Type objectType, CancellationToken cancellationToken = default)
+ {
+ serializer.SerializeObject(ref writer, value, shapeProvider.Resolve(objectType), cancellationToken);
+ }
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.FormatterContextBuilder.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.FormatterContextBuilder.cs
new file mode 100644
index 000000000..b511cbf22
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.FormatterContextBuilder.cs
@@ -0,0 +1,219 @@
+// 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.Collections.Immutable;
+using System.IO.Pipelines;
+using Nerdbank.MessagePack;
+using PolyType;
+using PolyType.Abstractions;
+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.
+///
+public sealed partial class NerdbankMessagePackFormatter
+{
+ ///
+ /// Provides methods to build a serialization context for the .
+ ///
+ public class FormatterContextBuilder
+ {
+ private readonly NerdbankMessagePackFormatter formatter;
+ private readonly FormatterContext baseContext;
+
+ private ImmutableArray.Builder? typeShapeProvidersBuilder = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The formatter to use.
+ /// The base context to build upon.
+ internal FormatterContextBuilder(NerdbankMessagePackFormatter formatter, FormatterContext baseContext)
+ {
+ this.formatter = formatter;
+ this.baseContext = baseContext;
+ }
+
+ ///
+ /// Adds a type shape provider to the context.
+ ///
+ /// The type shape provider to add.
+ public void AddTypeShapeProvider(ITypeShapeProvider provider)
+ {
+ this.typeShapeProvidersBuilder ??= ImmutableArray.CreateBuilder();
+ this.typeShapeProvidersBuilder.Add(provider);
+ }
+
+ ///
+ /// Registers an async enumerable type with the context.
+ ///
+ /// The type of the async enumerable.
+ /// The type of the elements in the async enumerable.
+ public void RegisterAsyncEnumerableType()
+ where TEnumerable : IAsyncEnumerable
+ {
+ MessagePackConverter converter = this.formatter.asyncEnumerableConverterResolver.GetConverter();
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers a converter with the context.
+ ///
+ /// The type the converter handles.
+ /// The converter to register.
+ public void RegisterConverter(MessagePackConverter converter)
+ {
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers known subtypes for a base type with the context.
+ ///
+ /// The base type.
+ /// The mapping of known subtypes.
+ public void RegisterKnownSubTypes(KnownSubTypeMapping mapping)
+ {
+ this.baseContext.Serializer.RegisterKnownSubTypes(mapping);
+ }
+
+ ///
+ /// Registers a progress type with the context.
+ ///
+ /// The type of the progress.
+ /// The type of the report.
+ public void RegisterProgressType()
+ where TProgress : IProgress
+ {
+ MessagePackConverter converter = this.formatter.progressConverterResolver.GetConverter();
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers a duplex pipe type with the context.
+ ///
+ /// The type of the duplex pipe.
+ public void RegisterDuplexPipeType()
+ where TPipe : IDuplexPipe
+ {
+ MessagePackConverter converter = this.formatter.pipeConverterResolver.GetConverter();
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers a pipe reader type with the context.
+ ///
+ /// The type of the pipe reader.
+ public void RegisterPipeReaderType()
+ where TReader : PipeReader
+ {
+ MessagePackConverter converter = this.formatter.pipeConverterResolver.GetConverter();
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers a pipe writer type with the context.
+ ///
+ /// The type of the pipe writer.
+ public void RegisterPipeWriterType()
+ where TWriter : PipeWriter
+ {
+ MessagePackConverter converter = this.formatter.pipeConverterResolver.GetConverter();
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers a stream type with the context.
+ ///
+ /// The type of the stream.
+ public void RegisterStreamType()
+ where TStream : Stream
+ {
+ MessagePackConverter converter = this.formatter.pipeConverterResolver.GetConverter();
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers an exception type with the context.
+ ///
+ /// The type of the exception.
+ public void RegisterExceptionType()
+ where TException : Exception
+ {
+ MessagePackConverter converter = this.formatter.exceptionResolver.GetConverter();
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ ///
+ /// Registers an RPC marshalable type with the context.
+ ///
+ /// The type to register.
+ public void RegisterRpcMarshalableType()
+ where T : class
+ {
+ if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(
+ typeof(T),
+ out JsonRpcProxyOptions? proxyOptions,
+ out JsonRpcTargetOptions? targetOptions,
+ out RpcMarshalableAttribute? attribute))
+ {
+ var converter = (RpcMarshalableConverter)Activator.CreateInstance(
+ typeof(RpcMarshalableConverter<>).MakeGenericType(typeof(T)),
+ this.formatter,
+ proxyOptions,
+ targetOptions,
+ attribute)!;
+
+ this.baseContext.Serializer.RegisterConverter(converter);
+ }
+
+ // TODO: Throw?
+ }
+
+ ///
+ /// Builds the formatter context.
+ ///
+ /// The built formatter context.
+ internal FormatterContext Build()
+ {
+ if (this.typeShapeProvidersBuilder is null || this.typeShapeProvidersBuilder.Count < 1)
+ {
+ return this.baseContext;
+ }
+
+ ITypeShapeProvider provider = this.typeShapeProvidersBuilder.Count == 1
+ ? this.typeShapeProvidersBuilder[0]
+ : new CompositeTypeShapeProvider(this.typeShapeProvidersBuilder.ToImmutable());
+
+ return new FormatterContext(this.baseContext.Serializer, provider);
+ }
+ }
+
+ private class CompositeTypeShapeProvider : ITypeShapeProvider
+ {
+ private readonly ImmutableArray providers;
+
+ internal CompositeTypeShapeProvider(ImmutableArray providers)
+ {
+ this.providers = providers;
+ }
+
+ public ITypeShape? GetShape(Type type)
+ {
+ foreach (ITypeShapeProvider provider in this.providers)
+ {
+ ITypeShape? shape = provider.GetShape(type);
+ if (shape is not null)
+ {
+ return shape;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs
new file mode 100644
index 000000000..204196138
--- /dev/null
+++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs
@@ -0,0 +1,2242 @@
+// 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.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 Nerdbank.Streams;
+using PolyType;
+using PolyType.Abstractions;
+using PolyType.ReflectionProvider;
+using PolyType.SourceGenerator;
+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.
+///
+public sealed partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcFormatterTracingCallbacks, IJsonRpcMessageFactory
+{
+ ///
+ /// The constant "jsonrpc", in its various forms.
+ ///
+ private static readonly CommonString VersionPropertyName = new(Constants.jsonrpc);
+
+ ///
+ /// The constant "id", in its various forms.
+ ///
+ private static readonly CommonString IdPropertyName = new(Constants.id);
+
+ ///
+ /// The constant "method", in its various forms.
+ ///
+ private static readonly CommonString MethodPropertyName = new(Constants.Request.method);
+
+ ///
+ /// The constant "result", in its various forms.
+ ///
+ private static readonly CommonString ResultPropertyName = new(Constants.Result.result);
+
+ ///
+ /// The constant "error", in its various forms.
+ ///
+ private static readonly CommonString ErrorPropertyName = new(Constants.Error.error);
+
+ ///
+ /// The constant "params", in its various forms.
+ ///
+ private static readonly CommonString ParamsPropertyName = new(Constants.Request.@params);
+
+ ///
+ /// The constant "traceparent", in its various forms.
+ ///
+ private static readonly CommonString TraceParentPropertyName = new(Constants.Request.traceparent);
+
+ ///
+ /// The constant "tracestate", in its various forms.
+ ///
+ private static readonly CommonString TraceStatePropertyName = new(Constants.Request.tracestate);
+
+ ///
+ /// The constant "2.0", in its various forms.
+ ///
+ private static readonly CommonString Version2 = new("2.0");
+
+ ///
+ /// 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 = new Dictionary>();
+
+ ///
+ /// The serializer context to use for top-level RPC messages.
+ ///
+ private readonly FormatterContext rpcContext;
+
+ private readonly ProgressConverterResolver progressConverterResolver;
+
+ private readonly AsyncEnumerableConverterResolver asyncEnumerableConverterResolver;
+
+ private readonly PipeConverterResolver pipeConverterResolver;
+
+ private readonly MessagePackExceptionConverterResolver exceptionResolver;
+
+ private readonly ToStringHelper serializationToStringHelper = new();
+
+ private readonly ToStringHelper deserializationToStringHelper = new();
+
+ ///
+ /// The serializer to use for user data (e.g. arguments, return values and errors).
+ ///
+ private FormatterContext userDataContext;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NerdbankMessagePackFormatter()
+ {
+ // Set up initial options for our own message types.
+ MessagePackSerializer serializer = new()
+ {
+ InternStrings = true,
+ SerializeDefaultValues = false,
+ };
+
+ serializer.RegisterConverter(new RequestIdConverter());
+ serializer.RegisterConverter(new JsonRpcMessageConverter(this));
+ serializer.RegisterConverter(new JsonRpcRequestConverter(this));
+ serializer.RegisterConverter(new JsonRpcResultConverter(this));
+ serializer.RegisterConverter(new JsonRpcErrorConverter(this));
+ serializer.RegisterConverter(new JsonRpcErrorDetailConverter(this));
+ serializer.RegisterConverter(new TraceParentConverter());
+
+ this.rpcContext = new FormatterContext(serializer, ShapeProvider_StreamJsonRpc.Default);
+
+ // Create the specialized formatters/resolvers that we will inject into the chain for user data.
+ this.progressConverterResolver = new ProgressConverterResolver(this);
+ this.asyncEnumerableConverterResolver = new AsyncEnumerableConverterResolver(this);
+ this.pipeConverterResolver = new PipeConverterResolver(this);
+ this.exceptionResolver = new MessagePackExceptionConverterResolver(this);
+
+ FormatterContext userDataContext = new(
+ new()
+ {
+ InternStrings = true,
+ SerializeDefaultValues = false,
+ },
+ ReflectionTypeShapeProvider.Default);
+
+ this.MassageUserDataContext(userDataContext);
+ this.userDataContext = userDataContext;
+ }
+
+ 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.
+ ///
+ ReadOnlySequence OriginalMessagePack { get; }
+ }
+
+ ///
+ /// Configures the serialization context for user data with the specified configuration action.
+ ///
+ /// The action to configure the serialization context.
+ public void SetFormatterContext(Action configure)
+ {
+ Requires.NotNull(configure, nameof(configure));
+
+ var builder = new FormatterContextBuilder(this, this.userDataContext);
+ configure(builder);
+
+ FormatterContext context = builder.Build();
+ this.MassageUserDataContext(context);
+
+ this.userDataContext = context;
+ }
+
+ ///
+ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer)
+ {
+ JsonRpcMessage message = this.rpcContext.Serializer.Deserialize(contentBuffer, ShapeProvider_StreamJsonRpc.Default)
+ ?? throw new MessagePackSerializationException(Resources.UnexpectedErrorProcessingJsonRpc);
+
+ IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc;
+ this.deserializationToStringHelper.Activate(contentBuffer);
+ try
+ {
+ tracingCallbacks?.OnMessageDeserialized(message, this.deserializationToStringHelper);
+ }
+ finally
+ {
+ this.deserializationToStringHelper.Deactivate();
+ }
+
+ return message;
+ }
+
+ ///
+ public void Serialize(IBufferWriter contentBuffer, JsonRpcMessage message)
+ {
+ if (message is Protocol.JsonRpcRequest request
+ && request.Arguments is not null
+ && request.ArgumentsList is null
+ && request.Arguments is not IReadOnlyDictionary)
+ {
+ // 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(contentBuffer);
+ try
+ {
+ this.rpcContext.Serializer.Serialize(ref writer, message, this.rpcContext.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.userDataContext);
+
+ ///
+ Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this, this.rpcContext);
+
+ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage message, ReadOnlySequence encodedMessage)
+ {
+ IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc;
+ this.serializationToStringHelper.Activate(encodedMessage);
+ try
+ {
+ tracingCallbacks?.OnMessageSerialized(message, this.serializationToStringHelper);
+ }
+ finally
+ {
+ this.serializationToStringHelper.Deactivate();
+ }
+ }
+
+ ///
+ /// 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 ? new Dictionary() : 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!);
+ }
+
+ private static ReadOnlySequence GetSliceForNextToken(ref MessagePackReader reader, in SerializationContext context)
+ {
+ SequencePosition startingPosition = reader.Position;
+ reader.Skip(context);
+ SequencePosition endingPosition = reader.Position;
+ return reader.Sequence.Slice(startingPosition, endingPosition);
+ }
+
+ ///
+ /// Reads a string with an optimized path for the value "2.0".
+ ///
+ /// The reader to use.
+ /// The decoded string.
+ private static unsafe string ReadProtocolVersion(ref MessagePackReader reader)
+ {
+ if (!reader.TryReadStringSpan(out ReadOnlySpan valueBytes))
+ {
+ // TODO: More specific exception type
+ throw new MessagePackSerializationException(Resources.UnexpectedErrorProcessingJsonRpc);
+ }
+
+ // Recognize "2.0" since we expect it and can avoid decoding and allocating a new string for it.
+ if (Version2.TryRead(valueBytes))
+ {
+ return Version2.Value;
+ }
+ else
+ {
+ // It wasn't the expected value, so decode it.
+ fixed (byte* pValueBytes = valueBytes)
+ {
+ return Encoding.UTF8.GetString(pValueBytes, valueBytes.Length);
+ }
+ }
+ }
+
+ ///
+ /// Writes the JSON-RPC version property name and value in a highly optimized way.
+ ///
+ private static void WriteProtocolVersionPropertyAndValue(ref MessagePackWriter writer, string version)
+ {
+ VersionPropertyName.Write(ref writer);
+ if (!Version2.TryWrite(ref writer, version))
+ {
+ writer.Write(version);
+ }
+ }
+
+ private static void ReadUnknownProperty(ref MessagePackReader reader, in SerializationContext context, ref Dictionary>? topLevelProperties, ReadOnlySpan stringKey)
+ {
+ topLevelProperties ??= new Dictionary>(StringComparer.Ordinal);
+#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
+ string name = Encoding.UTF8.GetString(stringKey);
+#else
+ string name = Encoding.UTF8.GetString(stringKey.ToArray());
+#endif
+ topLevelProperties.Add(name, GetSliceForNextToken(ref reader, context));
+ }
+
+ ///
+ /// Takes the user-supplied resolver for their data types and prepares the wrapping options
+ /// and the dynamic object wrapper for serialization.
+ ///
+ /// The options for user data that is supplied by the user (or the default).
+ private void MassageUserDataContext(FormatterContext userDataContext)
+ {
+ // Add our own resolvers to fill in specialized behavior if the user doesn't provide/override it by their own resolver.
+ userDataContext.Serializer.RegisterConverter(RequestIdConverter.Instance);
+ userDataContext.Serializer.RegisterConverter(EventArgsConverter.Instance);
+ }
+
+ private class MessagePackFormatterConverter : IFormatterConverter
+ {
+ private readonly FormatterContext context;
+
+ internal MessagePackFormatterConverter(FormatterContext formatterContext)
+ {
+ this.context = formatterContext;
+ }
+
+#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
+ {
+ return this.context.DeserializeObject((RawMessagePack)value, type);
+ }
+
+ public object Convert(object value, TypeCode typeCode)
+ {
+ return typeCode switch
+ {
+ TypeCode.Object => this.context.Deserialize