diff --git a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj index 9c400d8455..7efc519e10 100644 --- a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj +++ b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj @@ -24,6 +24,9 @@ This package provides the core platform and the .NET implementation of the proto + + + diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs new file mode 100644 index 0000000000..55e3f182d1 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json; + +using Microsoft.Testing.Platform.Extensions.Messages; + +namespace Microsoft.Testing.Platform.ServerMode.Json; + +internal sealed partial class Json +{ + private static void RegisterDefaultDeserializers(Dictionary deserializers) + { + // Deserializers + deserializers[typeof(string)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetString()!); + deserializers[typeof(bool)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetBoolean()); + deserializers[typeof(int)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetInt32()); + deserializers[typeof(decimal)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDecimal()); + deserializers[typeof(DateTime)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDateTime()); + + deserializers[typeof(IDictionary)] = new JsonElementDeserializer>((json, jsonDocument) => + { + Dictionary items = []; + foreach (JsonProperty kvp in jsonDocument.EnumerateObject()) + { + switch (kvp.Value.ValueKind) + { + case JsonValueKind.String: + items.Add(kvp.Name, kvp.Value.GetString()); + break; + case JsonValueKind.Number: + items.Add(kvp.Name, kvp.Value.GetInt32()); + break; + case JsonValueKind.True: + items.Add(kvp.Name, true); + break; + case JsonValueKind.False: + items.Add(kvp.Name, false); + break; + case JsonValueKind.Object: + items.Add(kvp.Name, json.Bind>(kvp.Value)); + break; + case JsonValueKind.Array: + items.Add(kvp.Name, json.Bind(kvp.Value)); + break; + case JsonValueKind.Null: + items.Add(kvp.Name, null); + break; + default: + throw new InvalidOperationException($"key: {kvp.Name}, value: {kvp.Value}, type: {kvp.Value.ValueKind}"); + } + } + + return items; + }); + + deserializers[typeof(RpcMessage)] = new JsonElementDeserializer((json, jsonElement) => + { + ValidateJsonRpcHeader(json, jsonElement); + + if (json.TryBind(jsonElement, out string? method, JsonRpcStrings.Method)) + { + bool hasId = json.TryBind(jsonElement, out int id, JsonRpcStrings.Id); + + object? @params = null; + if (jsonElement.TryGetProperty(JsonRpcStrings.Params, out JsonElement value)) + { + try + { + // Parse the specific methods + @params = method switch + { + JsonRpcMethods.Initialize => json.Bind(value), + JsonRpcMethods.TestingDiscoverTests => json.Bind(value), + JsonRpcMethods.TestingRunTests => json.Bind(value), + JsonRpcMethods.CancelRequest => json.Bind(value), + JsonRpcMethods.Exit => json.Bind(value), + + // Note: Let the server report unknown RPC request back to the client. + _ => null, + }; + } + catch (Exception ex) when (ex is MessageFormatException or InvalidOperationException or JsonException) + { + // If params can't be deserialized for a request, capture the failure so + // we can later send back a properly coded JSON-RPC error using the request id. + // For notifications there's no one to respond to, but we still avoid + // crashing the message-handling loop by swallowing into the sentinel. + // We catch the broader set of deserialization-related exceptions because the + // request payload is untrusted client input and the lower-level helpers + // (e.g. JsonElement.GetString() on a non-string element) can throw types + // other than MessageFormatException. + @params = new InvalidRequestParamsArgs(ErrorCodes.InvalidParams, ex.Message); + } + } + + return hasId + ? new RequestMessage(id, method!, @params) + : new NotificationMessage(method!, @params); + } + + if (jsonElement.TryGetProperty(JsonRpcStrings.Result, out JsonElement element)) + { + // Note: Because the result message does not contain the original method name, + // it's not possible for us to do a typed deserialization. + // The best option we've got is to return a generic property bag. + int id = json.Bind(jsonElement, JsonRpcStrings.Id); + + IDictionary? result = element.ValueKind == JsonValueKind.Null ? null : + json.Bind>(jsonElement, JsonRpcStrings.Result); + + return new ResponseMessage(id, result); + } + + return json.TryBind(jsonElement, out ErrorMessage? errorMessage) ? errorMessage! : throw new MessageFormatException(); + }); + + deserializers[typeof(InitializeRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => new InitializeRequestArgs( + ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), + ClientInfo: json.Bind(jsonElement, JsonRpcStrings.ClientInfo), + Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); + + deserializers[typeof(ClientInfo)] = new JsonElementDeserializer((json, jsonElement) => new ClientInfo( + Name: json.Bind(jsonElement, JsonRpcStrings.Name), + Version: json.Bind(jsonElement, JsonRpcStrings.Version))); + + deserializers[typeof(ClientCapabilities)] = new JsonElementDeserializer((json, jsonElement) => + { + jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing); + + return new ClientCapabilities( + DebuggerProvider: json.Bind(testing, JsonRpcStrings.DebuggerProvider)); + }); + + deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer( + (json, jsonElement) => new InitializeResponseArgs( + ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), + ServerInfo: json.Bind(jsonElement, JsonRpcStrings.ServerInfo), + Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); + + deserializers[typeof(ServerInfo)] = new JsonElementDeserializer( + (json, jsonElement) => new ServerInfo( + Name: json.Bind(jsonElement, JsonRpcStrings.Name), + Version: json.Bind(jsonElement, JsonRpcStrings.Version))); + + deserializers[typeof(ServerCapabilities)] = new JsonElementDeserializer( + (json, jsonElement) => new ServerCapabilities( + TestingCapabilities: json.Bind(jsonElement, JsonRpcStrings.Testing))); + + deserializers[typeof(ServerTestingCapabilities)] = new JsonElementDeserializer( + (json, jsonElement) => new ServerTestingCapabilities( + SupportsDiscovery: json.Bind(jsonElement, JsonRpcStrings.SupportsDiscovery), + MultiRequestSupport: json.Bind(jsonElement, JsonRpcStrings.MultiRequestSupport), + VSTestProviderSupport: json.Bind(jsonElement, JsonRpcStrings.VSTestProviderSupport), + SupportsAttachments: json.Bind(jsonElement, JsonRpcStrings.AttachmentsSupport), + MultiConnectionProvider: json.Bind(jsonElement, JsonRpcStrings.MultiConnectionProvider))); + + deserializers[typeof(DiscoverRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => + { + string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); + if (!Guid.TryParse(runId, out Guid result)) + { + throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); + } + + json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); + json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); + + return new DiscoverRequestArgs( + RunId: result, + TestNodes: testNodes, + GraphFilter: graphFilter); + }); + + deserializers[typeof(RunRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => + { + string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); + if (!Guid.TryParse(runId, out Guid result)) + { + throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); + } + + json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); + json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); + + return new RunRequestArgs( + RunId: result, + TestNodes: testNodes, + GraphFilter: graphFilter); + }); + + deserializers[typeof(TestNode)] = new JsonElementDeserializer( + (json, properties) => + { + PropertyBag propertyBag = new(); + string uid = json.Bind(properties, JsonRpcStrings.Uid) ?? string.Empty; + string displayName = json.Bind(properties, JsonRpcStrings.DisplayName); + + if (json.TryBind(properties, out string? locationFile, "location.file")) + { + json.TryBind(properties, out int locationLineStart, "location.line-start"); + json.TryBind(properties, out int locationLineEnd, "location.line-end"); + + TestFileLocationProperty testFileLocationProperty = new( + locationFile!, + new LinePositionSpan(new LinePosition(locationLineStart, 0), new LinePosition(locationLineEnd, 0))); + propertyBag.Add(testFileLocationProperty); + } + + return new TestNode + { + Uid = new TestNodeUid(uid), + DisplayName = displayName, + Properties = propertyBag, + }; + }); + + deserializers[typeof(CancelRequestArgs)] = new JsonElementDeserializer( + (json, jsonElement) => json.TryBind(jsonElement, out int id, JsonRpcStrings.Id) ? new CancelRequestArgs(id) : throw new MessageFormatException("id field should be an int")); + + deserializers[typeof(ExitRequestArgs)] = new JsonElementDeserializer( + (json, jsonElement) => new ExitRequestArgs()); + + deserializers[typeof(ErrorMessage)] = new JsonElementDeserializer( + (json, jsonElement) => + { + ValidateJsonRpcHeader(json, jsonElement); + + int id = json.Bind(jsonElement, JsonRpcStrings.Id); + JsonElement error = jsonElement.GetProperty(JsonRpcStrings.Error); + + int code = json.Bind(error, JsonRpcStrings.Code); + string message = json.Bind(error, JsonRpcStrings.Message); + + if (json.TryBind(error, out IDictionary? data, JsonRpcStrings.Data) && data?.Count == 0) + { + data = null; + } + + return new ErrorMessage( + Id: id, + ErrorCode: code, + Message: message ?? string.Empty, + Data: data); + }); + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs new file mode 100644 index 0000000000..367987c8ac --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Extensions.Messages; + +namespace Microsoft.Testing.Platform.ServerMode.Json; + +internal sealed partial class Json +{ + private static void RegisterDefaultSerializers(Dictionary serializers) + { + // Overridden default serializers for better performance using .NET runtime serialization APIs + + // Serialize response types. + serializers[typeof(RequestMessage)] = new JsonObjectSerializer(request => + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Id, request.Id), + (JsonRpcStrings.Method, request.Method), + (JsonRpcStrings.Params, request.Params) + ]); + + serializers[typeof(ResponseMessage)] = new JsonObjectSerializer(response => + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Id, response.Id), + (JsonRpcStrings.Result, response.Result) + ]); + + serializers[typeof(NotificationMessage)] = new JsonObjectSerializer(notification => + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Method, notification.Method), + (JsonRpcStrings.Params, notification.Params) + ]); + + serializers[typeof(ErrorMessage)] = new JsonObjectSerializer(error => + { + var errorMsg = new (string, object?)[] + { + (JsonRpcStrings.Code, error.ErrorCode), + (JsonRpcStrings.Data, error.Data ?? new()), + (JsonRpcStrings.Message, error.Message), + }; + + return + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Code, error.ErrorCode), + (JsonRpcStrings.Id, error.Id), + (JsonRpcStrings.Error, errorMsg) + ]; + }); + + serializers[typeof(InitializeResponseArgs)] = new JsonObjectSerializer(response => + [ + (JsonRpcStrings.ProcessId, response.ProcessId), + (JsonRpcStrings.ServerInfo, response.ServerInfo), + (JsonRpcStrings.Capabilities, response.Capabilities) + ]); + + serializers[typeof(ServerInfo)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Name, info.Name), + (JsonRpcStrings.Version, info.Version) + ]); + + serializers[typeof(ServerCapabilities)] = new JsonObjectSerializer(capabilities => + [ + (JsonRpcStrings.Testing, capabilities.TestingCapabilities) + ]); + + serializers[typeof(ServerTestingCapabilities)] = new JsonObjectSerializer(capabilities => + [ + (JsonRpcStrings.SupportsDiscovery, capabilities.SupportsDiscovery), + (JsonRpcStrings.MultiRequestSupport, capabilities.MultiRequestSupport), + (JsonRpcStrings.VSTestProviderSupport, capabilities.VSTestProviderSupport), + (JsonRpcStrings.AttachmentsSupport, capabilities.SupportsAttachments), + (JsonRpcStrings.MultiConnectionProvider, capabilities.MultiConnectionProvider) + ]); + + serializers[typeof(Artifact)] = new JsonObjectSerializer(artifact => + [ + (JsonRpcStrings.Uri, artifact.Uri), + (JsonRpcStrings.Producer, artifact.Producer), + (JsonRpcStrings.Type, artifact.Type), + (JsonRpcStrings.DisplayName, artifact.DisplayName), + (JsonRpcStrings.Description, artifact.Description) + ]); + + serializers[typeof(DiscoverResponseArgs)] = new JsonObjectSerializer(response => []); + + serializers[typeof(RunResponseArgs)] = new JsonObjectSerializer(response => + [ + (JsonRpcStrings.Attachments, response.Artifacts) + ]); + + serializers[typeof(TestNodeUpdateMessage)] = new JsonObjectSerializer(message => + [ + (JsonRpcStrings.Node, message.TestNode), + (JsonRpcStrings.Parent, message.ParentTestNodeUid?.Value) + ]); + + serializers[typeof(TestNodeStateChangedEventArgs)] = new JsonObjectSerializer(message => + [ + (JsonRpcStrings.RunId, message.RunId), + (JsonRpcStrings.Changes, message.Changes) + ]); + + serializers[typeof(TestNode)] = new JsonObjectSerializer(BuildTestNodeProperties); + + serializers[typeof(LogEventArgs)] = new JsonObjectSerializer(message => + [ + (JsonRpcStrings.Level, message.LogMessage.Level.ToString()), + (JsonRpcStrings.Message, message.LogMessage.Message) + ]); + + serializers[typeof(CancelRequestArgs)] = new JsonObjectSerializer(request => + [ + (JsonRpcStrings.Id, request.CancelRequestId) + ]); + + serializers[typeof(TelemetryEventArgs)] = new JsonObjectSerializer(ev => + [ + (JsonRpcStrings.EventName, ev.EventName), + (JsonRpcStrings.Metrics, ev.Metrics) + ]); + + serializers[typeof(ProcessInfoArgs)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Program, info.Program), + (JsonRpcStrings.Args, info.Args), + (JsonRpcStrings.WorkingDirectory, info.WorkingDirectory), + (JsonRpcStrings.EnvironmentVariables, info.EnvironmentVariables) + ]); + + serializers[typeof(AttachDebuggerInfoArgs)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.ProcessId, info.ProcessId) + ]); + + serializers[typeof(TestsAttachments)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Attachments, info.Attachments) + ]); + + serializers[typeof(RunTestAttachment)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Uri, info.Uri), + (JsonRpcStrings.Producer, info.Producer), + (JsonRpcStrings.Type, info.Type), + (JsonRpcStrings.DisplayName, info.DisplayName), + (JsonRpcStrings.Description, info.Description) + ]); + + // Serializers + serializers[typeof(string)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); + serializers[typeof(bool)] = new JsonValueSerializer((w, v) => w.WriteBooleanValue(v)); + serializers[typeof(char)] = new JsonValueSerializer((w, v) => w.WriteStringValue($"{v}")); + serializers[typeof(int)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(long)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(float)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(double)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(decimal)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(Guid)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); + serializers[typeof(DateTime)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); + serializers[typeof(DateTimeOffset)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); + + // serializers[typeof(TimeSpan)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v.ToString())); // Remove for now + serializers[typeof((string, object?)[])] = new JsonObjectSerializer<(string, object?)[]>(n => n); + serializers[typeof(Dictionary)] = new JsonObjectSerializer>(d => [.. d.Select(kvp => (kvp.Key, (object?)kvp.Value))]); + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs new file mode 100644 index 0000000000..30668285a0 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Extensions.Messages; + +namespace Microsoft.Testing.Platform.ServerMode.Json; + +internal sealed partial class Json +{ + private static (string Name, object? Value)[] BuildTestNodeProperties(TestNode message) + { + List<(string Name, object? Value)> properties = + [ + with(capacity: 16), + (JsonRpcStrings.Uid, message.Uid.Value), + (JsonRpcStrings.DisplayName, message.DisplayName) + ]; + + List>? traits = null; + bool hasActionNodeType = false; + + int attachmentIndex = 0; + foreach (IProperty property in message.Properties) + { + if (property is TestMetadataProperty metadataProperty) + { + (traits ??= []).Add(new KeyValuePair(metadataProperty.Key, metadataProperty.Value)); + continue; + } + + if (property is SerializableKeyValuePairStringProperty keyValuePairProperty) + { + properties.Add((keyValuePairProperty.Key, keyValuePairProperty.Value)); + continue; + } + + if (property is TestFileLocationProperty fileLocationProperty) + { + properties.Add(("location.file", fileLocationProperty.FilePath)); + properties.Add(("location.line-start", fileLocationProperty.LineSpan.Start.Line)); + properties.Add(("location.line-end", fileLocationProperty.LineSpan.End.Line)); + continue; + } + + if (property is TestMethodIdentifierProperty testMethodIdentifierProperty) + { + properties.Add(("location.type", RoslynString.IsNullOrEmpty(testMethodIdentifierProperty.Namespace) + ? testMethodIdentifierProperty.TypeName + : $"{testMethodIdentifierProperty.Namespace}.{testMethodIdentifierProperty.TypeName}")); + + properties.Add(("location.method", testMethodIdentifierProperty.ParameterTypeFullNames.Length > 0 + ? $"{testMethodIdentifierProperty.MethodName}({string.Join(',', testMethodIdentifierProperty.ParameterTypeFullNames)})" + : testMethodIdentifierProperty.MethodName)); + + properties.Add(("location.method-arity", testMethodIdentifierProperty.MethodArity)); + + continue; + } + + if (property is StandardOutputProperty standardOutputProperty) + { + properties.Add(("standardOutput", standardOutputProperty.StandardOutput)); + } + + if (property is StandardErrorProperty standardErrorProperty) + { + properties.Add(("standardError", standardErrorProperty.StandardError)); + } + + if (property is TestNodeStateProperty testNodeStateProperty) + { + properties.Add(("node-type", "action")); + hasActionNodeType = true; + switch (property) + { + case DiscoveredTestNodeStateProperty: + { + properties.Add(("execution-state", "discovered")); + break; + } + + case InProgressTestNodeStateProperty: + { + properties.Add(("execution-state", "in-progress")); + break; + } + + case PassedTestNodeStateProperty: + { + properties.Add(("execution-state", "passed")); + break; + } + + case SkippedTestNodeStateProperty skippedTestNodeStateProperty: + { + properties.Add(("execution-state", "skipped")); + if (!RoslynString.IsNullOrEmpty(skippedTestNodeStateProperty.Explanation)) + { + properties.Add(("error.message", skippedTestNodeStateProperty.Explanation)); + } + + break; + } + + case FailedTestNodeStateProperty failedTestNodeStateProperty: + { + properties.Add(("execution-state", "failed")); + Exception? exception = failedTestNodeStateProperty.Exception; + properties.Add(("error.message", failedTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + properties.Add(("assert.actual", exception.Data["assert.actual"] ?? string.Empty)); + properties.Add(("assert.expected", exception.Data["assert.expected"] ?? string.Empty)); + } + + break; + } + + case TimeoutTestNodeStateProperty timeoutTestNodeStateProperty: + { + properties.Add(("execution-state", "timed-out")); + Exception? exception = timeoutTestNodeStateProperty.Exception; + properties.Add(("error.message", timeoutTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + } + + break; + } + + case ErrorTestNodeStateProperty errorTestNodeStateProperty: + { + properties.Add(("execution-state", "error")); + Exception? exception = errorTestNodeStateProperty.Exception; + properties.Add(("error.message", errorTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + } + + break; + } + +#pragma warning disable CS0618, MTP0001 // Type or member is obsolete + case CancelledTestNodeStateProperty canceledTestNodeStateProperty: +#pragma warning restore CS0618, MTP0001 // Type or member is obsolete + { + properties.Add(("execution-state", "canceled")); + Exception? exception = canceledTestNodeStateProperty.Exception; + properties.Add(("error.message", canceledTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + } + + break; + } + + default: + throw new NotSupportedException($"Unsupported TestNodeStateProperty '{testNodeStateProperty.GetType()}'"); + } + + continue; + } + + if (property is TimingProperty timingProperty) + { + properties.Add(("time.duration-ms", timingProperty.GlobalTiming.Duration.TotalMilliseconds)); + continue; + } + + if (property is FileArtifactProperty artifact) + { + properties.Add(($"attachments.{attachmentIndex}.uri", artifact.FileInfo.FullName)); + properties.Add(($"attachments.{attachmentIndex}.display-name", artifact.DisplayName)); + properties.Add(($"attachments.{attachmentIndex}.description", artifact.Description)); + attachmentIndex++; + continue; + } + } + + if (traits is not null) + { + // Insert "traits" right after "uid" and "display-name" to preserve the + // original wire format ordering. + properties.Insert(2, ("traits", traits)); + } + + if (!hasActionNodeType) + { + properties.Add(("node-type", "group")); + } + + return [.. properties]; + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs index b1ef45bc4d..53db3220ae 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs @@ -3,12 +3,11 @@ using System.Text.Json; -using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Helpers; namespace Microsoft.Testing.Platform.ServerMode.Json; -internal sealed class Json +internal sealed partial class Json { private readonly Dictionary _deserializers = []; private readonly Dictionary _serializers = []; @@ -16,586 +15,8 @@ internal sealed class Json public Json(Dictionary? serializers = null, Dictionary? deserializers = null) { - // Overridden default serializers for better performance using .NET runtime serialization APIs - - // Serialize response types. - _serializers[typeof(RequestMessage)] = new JsonObjectSerializer(request => - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Id, request.Id), - (JsonRpcStrings.Method, request.Method), - (JsonRpcStrings.Params, request.Params) - ]); - - _serializers[typeof(ResponseMessage)] = new JsonObjectSerializer(response => - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Id, response.Id), - (JsonRpcStrings.Result, response.Result) - ]); - - _serializers[typeof(NotificationMessage)] = new JsonObjectSerializer(notification => - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Method, notification.Method), - (JsonRpcStrings.Params, notification.Params) - ]); - - _serializers[typeof(ErrorMessage)] = new JsonObjectSerializer(error => - { - var errorMsg = new (string, object?)[] - { - (JsonRpcStrings.Code, error.ErrorCode), - (JsonRpcStrings.Data, error.Data ?? new()), - (JsonRpcStrings.Message, error.Message), - }; - - return - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Code, error.ErrorCode), - (JsonRpcStrings.Id, error.Id), - (JsonRpcStrings.Error, errorMsg) - ]; - }); - - _serializers[typeof(InitializeResponseArgs)] = new JsonObjectSerializer(response => - [ - (JsonRpcStrings.ProcessId, response.ProcessId), - (JsonRpcStrings.ServerInfo, response.ServerInfo), - (JsonRpcStrings.Capabilities, response.Capabilities) - ]); - - _serializers[typeof(ServerInfo)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Name, info.Name), - (JsonRpcStrings.Version, info.Version) - ]); - - _serializers[typeof(ServerCapabilities)] = new JsonObjectSerializer(capabilities => - [ - (JsonRpcStrings.Testing, capabilities.TestingCapabilities) - ]); - - _serializers[typeof(ServerTestingCapabilities)] = new JsonObjectSerializer(capabilities => - [ - (JsonRpcStrings.SupportsDiscovery, capabilities.SupportsDiscovery), - (JsonRpcStrings.MultiRequestSupport, capabilities.MultiRequestSupport), - (JsonRpcStrings.VSTestProviderSupport, capabilities.VSTestProviderSupport), - (JsonRpcStrings.AttachmentsSupport, capabilities.SupportsAttachments), - (JsonRpcStrings.MultiConnectionProvider, capabilities.MultiConnectionProvider) - ]); - - _serializers[typeof(Artifact)] = new JsonObjectSerializer(artifact => - [ - (JsonRpcStrings.Uri, artifact.Uri), - (JsonRpcStrings.Producer, artifact.Producer), - (JsonRpcStrings.Type, artifact.Type), - (JsonRpcStrings.DisplayName, artifact.DisplayName), - (JsonRpcStrings.Description, artifact.Description) - ]); - - _serializers[typeof(DiscoverResponseArgs)] = new JsonObjectSerializer(response => []); - - _serializers[typeof(RunResponseArgs)] = new JsonObjectSerializer(response => - [ - (JsonRpcStrings.Attachments, response.Artifacts) - ]); - - _serializers[typeof(TestNodeUpdateMessage)] = new JsonObjectSerializer(message => - [ - (JsonRpcStrings.Node, message.TestNode), - (JsonRpcStrings.Parent, message.ParentTestNodeUid?.Value) - ]); - - _serializers[typeof(TestNodeStateChangedEventArgs)] = new JsonObjectSerializer(message => - [ - (JsonRpcStrings.RunId, message.RunId), - (JsonRpcStrings.Changes, message.Changes) - ]); - - _serializers[typeof(TestNode)] = new JsonObjectSerializer(message => - { - List<(string Name, object? Value)> properties = - [ - with(capacity: 16), - (JsonRpcStrings.Uid, message.Uid.Value), - (JsonRpcStrings.DisplayName, message.DisplayName) - ]; - - List>? traits = null; - bool hasActionNodeType = false; - - int attachmentIndex = 0; - foreach (IProperty property in message.Properties) - { - if (property is TestMetadataProperty metadataProperty) - { - (traits ??= []).Add(new KeyValuePair(metadataProperty.Key, metadataProperty.Value)); - continue; - } - - if (property is SerializableKeyValuePairStringProperty keyValuePairProperty) - { - properties.Add((keyValuePairProperty.Key, keyValuePairProperty.Value)); - continue; - } - - if (property is TestFileLocationProperty fileLocationProperty) - { - properties.Add(("location.file", fileLocationProperty.FilePath)); - properties.Add(("location.line-start", fileLocationProperty.LineSpan.Start.Line)); - properties.Add(("location.line-end", fileLocationProperty.LineSpan.End.Line)); - continue; - } - - if (property is TestMethodIdentifierProperty testMethodIdentifierProperty) - { - properties.Add(("location.type", RoslynString.IsNullOrEmpty(testMethodIdentifierProperty.Namespace) - ? testMethodIdentifierProperty.TypeName - : $"{testMethodIdentifierProperty.Namespace}.{testMethodIdentifierProperty.TypeName}")); - - properties.Add(("location.method", testMethodIdentifierProperty.ParameterTypeFullNames.Length > 0 - ? $"{testMethodIdentifierProperty.MethodName}({string.Join(',', testMethodIdentifierProperty.ParameterTypeFullNames)})" - : testMethodIdentifierProperty.MethodName)); - - properties.Add(("location.method-arity", testMethodIdentifierProperty.MethodArity)); - - continue; - } - - if (property is StandardOutputProperty standardOutputProperty) - { - properties.Add(("standardOutput", standardOutputProperty.StandardOutput)); - } - - if (property is StandardErrorProperty standardErrorProperty) - { - properties.Add(("standardError", standardErrorProperty.StandardError)); - } - - if (property is TestNodeStateProperty testNodeStateProperty) - { - properties.Add(("node-type", "action")); - hasActionNodeType = true; - switch (property) - { - case DiscoveredTestNodeStateProperty: - { - properties.Add(("execution-state", "discovered")); - break; - } - - case InProgressTestNodeStateProperty: - { - properties.Add(("execution-state", "in-progress")); - break; - } - - case PassedTestNodeStateProperty: - { - properties.Add(("execution-state", "passed")); - break; - } - - case SkippedTestNodeStateProperty skippedTestNodeStateProperty: - { - properties.Add(("execution-state", "skipped")); - if (!RoslynString.IsNullOrEmpty(skippedTestNodeStateProperty.Explanation)) - { - properties.Add(("error.message", skippedTestNodeStateProperty.Explanation)); - } - - break; - } - - case FailedTestNodeStateProperty failedTestNodeStateProperty: - { - properties.Add(("execution-state", "failed")); - Exception? exception = failedTestNodeStateProperty.Exception; - properties.Add(("error.message", failedTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - properties.Add(("assert.actual", exception.Data["assert.actual"] ?? string.Empty)); - properties.Add(("assert.expected", exception.Data["assert.expected"] ?? string.Empty)); - } - - break; - } - - case TimeoutTestNodeStateProperty timeoutTestNodeStateProperty: - { - properties.Add(("execution-state", "timed-out")); - Exception? exception = timeoutTestNodeStateProperty.Exception; - properties.Add(("error.message", timeoutTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - } - - break; - } - - case ErrorTestNodeStateProperty errorTestNodeStateProperty: - { - properties.Add(("execution-state", "error")); - Exception? exception = errorTestNodeStateProperty.Exception; - properties.Add(("error.message", errorTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - } - - break; - } - -#pragma warning disable CS0618, MTP0001 // Type or member is obsolete - case CancelledTestNodeStateProperty canceledTestNodeStateProperty: -#pragma warning restore CS0618, MTP0001 // Type or member is obsolete - { - properties.Add(("execution-state", "canceled")); - Exception? exception = canceledTestNodeStateProperty.Exception; - properties.Add(("error.message", canceledTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - } - - break; - } - - default: - throw new NotSupportedException($"Unsupported TestNodeStateProperty '{testNodeStateProperty.GetType()}'"); - } - - continue; - } - - if (property is TimingProperty timingProperty) - { - properties.Add(("time.duration-ms", timingProperty.GlobalTiming.Duration.TotalMilliseconds)); - continue; - } - - if (property is FileArtifactProperty artifact) - { - properties.Add(($"attachments.{attachmentIndex}.uri", artifact.FileInfo.FullName)); - properties.Add(($"attachments.{attachmentIndex}.display-name", artifact.DisplayName)); - properties.Add(($"attachments.{attachmentIndex}.description", artifact.Description)); - attachmentIndex++; - continue; - } - } - - if (traits is not null) - { - // Insert "traits" right after "uid" and "display-name" to preserve the - // original wire format ordering. - properties.Insert(2, ("traits", traits)); - } - - if (!hasActionNodeType) - { - properties.Add(("node-type", "group")); - } - - return [.. properties]; - }); - - _serializers[typeof(LogEventArgs)] = new JsonObjectSerializer(message => - [ - (JsonRpcStrings.Level, message.LogMessage.Level.ToString()), - (JsonRpcStrings.Message, message.LogMessage.Message) - ]); - - _serializers[typeof(CancelRequestArgs)] = new JsonObjectSerializer(request => - [ - (JsonRpcStrings.Id, request.CancelRequestId) - ]); - - _serializers[typeof(TelemetryEventArgs)] = new JsonObjectSerializer(ev => - [ - (JsonRpcStrings.EventName, ev.EventName), - (JsonRpcStrings.Metrics, ev.Metrics) - ]); - - _serializers[typeof(ProcessInfoArgs)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Program, info.Program), - (JsonRpcStrings.Args, info.Args), - (JsonRpcStrings.WorkingDirectory, info.WorkingDirectory), - (JsonRpcStrings.EnvironmentVariables, info.EnvironmentVariables) - ]); - - _serializers[typeof(AttachDebuggerInfoArgs)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.ProcessId, info.ProcessId) - ]); - - _serializers[typeof(TestsAttachments)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Attachments, info.Attachments) - ]); - - _serializers[typeof(RunTestAttachment)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Uri, info.Uri), - (JsonRpcStrings.Producer, info.Producer), - (JsonRpcStrings.Type, info.Type), - (JsonRpcStrings.DisplayName, info.DisplayName), - (JsonRpcStrings.Description, info.Description) - ]); - - // Serializers - _serializers[typeof(string)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); - _serializers[typeof(bool)] = new JsonValueSerializer((w, v) => w.WriteBooleanValue(v)); - _serializers[typeof(char)] = new JsonValueSerializer((w, v) => w.WriteStringValue($"{v}")); - _serializers[typeof(int)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(long)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(float)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(double)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(decimal)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(Guid)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); - _serializers[typeof(DateTime)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); - _serializers[typeof(DateTimeOffset)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); - - // _serializers[typeof(TimeSpan)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v.ToString())); // Remove for now - _serializers[typeof((string, object?)[])] = new JsonObjectSerializer<(string, object?)[]>(n => n); - _serializers[typeof(Dictionary)] = new JsonObjectSerializer>(d => [.. d.Select(kvp => (kvp.Key, (object?)kvp.Value))]); - - // Deserializers - _deserializers[typeof(string)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetString()!); - _deserializers[typeof(bool)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetBoolean()); - _deserializers[typeof(int)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetInt32()); - _deserializers[typeof(decimal)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDecimal()); - _deserializers[typeof(DateTime)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDateTime()); - - _deserializers[typeof(IDictionary)] = new JsonElementDeserializer>((json, jsonDocument) => - { - Dictionary items = []; - foreach (JsonProperty kvp in jsonDocument.EnumerateObject()) - { - switch (kvp.Value.ValueKind) - { - case JsonValueKind.String: - items.Add(kvp.Name, kvp.Value.GetString()); - break; - case JsonValueKind.Number: - items.Add(kvp.Name, kvp.Value.GetInt32()); - break; - case JsonValueKind.True: - items.Add(kvp.Name, true); - break; - case JsonValueKind.False: - items.Add(kvp.Name, false); - break; - case JsonValueKind.Object: - items.Add(kvp.Name, json.Bind>(kvp.Value)); - break; - case JsonValueKind.Array: - items.Add(kvp.Name, json.Bind(kvp.Value)); - break; - case JsonValueKind.Null: - items.Add(kvp.Name, null); - break; - default: - throw new InvalidOperationException($"key: {kvp.Name}, value: {kvp.Value}, type: {kvp.Value.ValueKind}"); - } - } - - return items; - }); - - _deserializers[typeof(RpcMessage)] = new JsonElementDeserializer((json, jsonElement) => - { - ValidateJsonRpcHeader(json, jsonElement); - - if (json.TryBind(jsonElement, out string? method, JsonRpcStrings.Method)) - { - bool hasId = json.TryBind(jsonElement, out int id, JsonRpcStrings.Id); - - object? @params = null; - if (jsonElement.TryGetProperty(JsonRpcStrings.Params, out JsonElement value)) - { - try - { - // Parse the specific methods - @params = method switch - { - JsonRpcMethods.Initialize => json.Bind(value), - JsonRpcMethods.TestingDiscoverTests => json.Bind(value), - JsonRpcMethods.TestingRunTests => json.Bind(value), - JsonRpcMethods.CancelRequest => json.Bind(value), - JsonRpcMethods.Exit => json.Bind(value), - - // Note: Let the server report unknown RPC request back to the client. - _ => null, - }; - } - catch (Exception ex) when (ex is MessageFormatException or InvalidOperationException or JsonException) - { - // If params can't be deserialized for a request, capture the failure so - // we can later send back a properly coded JSON-RPC error using the request id. - // For notifications there's no one to respond to, but we still avoid - // crashing the message-handling loop by swallowing into the sentinel. - // We catch the broader set of deserialization-related exceptions because the - // request payload is untrusted client input and the lower-level helpers - // (e.g. JsonElement.GetString() on a non-string element) can throw types - // other than MessageFormatException. - @params = new InvalidRequestParamsArgs(ErrorCodes.InvalidParams, ex.Message); - } - } - - return hasId - ? new RequestMessage(id, method!, @params) - : new NotificationMessage(method!, @params); - } - - if (jsonElement.TryGetProperty(JsonRpcStrings.Result, out JsonElement element)) - { - // Note: Because the result message does not contain the original method name, - // it's not possible for us to do a typed deserialization. - // The best option we've got is to return a generic property bag. - int id = json.Bind(jsonElement, JsonRpcStrings.Id); - - IDictionary? result = element.ValueKind == JsonValueKind.Null ? null : - json.Bind>(jsonElement, JsonRpcStrings.Result); - - return new ResponseMessage(id, result); - } - - return json.TryBind(jsonElement, out ErrorMessage? errorMessage) ? errorMessage! : throw new MessageFormatException(); - }); - - _deserializers[typeof(InitializeRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => new InitializeRequestArgs( - ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), - ClientInfo: json.Bind(jsonElement, JsonRpcStrings.ClientInfo), - Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); - - _deserializers[typeof(ClientInfo)] = new JsonElementDeserializer((json, jsonElement) => new ClientInfo( - Name: json.Bind(jsonElement, JsonRpcStrings.Name), - Version: json.Bind(jsonElement, JsonRpcStrings.Version))); - - _deserializers[typeof(ClientCapabilities)] = new JsonElementDeserializer((json, jsonElement) => - { - jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing); - - return new ClientCapabilities( - DebuggerProvider: json.Bind(testing, JsonRpcStrings.DebuggerProvider)); - }); - - _deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer( - (json, jsonElement) => new InitializeResponseArgs( - ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), - ServerInfo: json.Bind(jsonElement, JsonRpcStrings.ServerInfo), - Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); - - _deserializers[typeof(ServerInfo)] = new JsonElementDeserializer( - (json, jsonElement) => new ServerInfo( - Name: json.Bind(jsonElement, JsonRpcStrings.Name), - Version: json.Bind(jsonElement, JsonRpcStrings.Version))); - - _deserializers[typeof(ServerCapabilities)] = new JsonElementDeserializer( - (json, jsonElement) => new ServerCapabilities( - TestingCapabilities: json.Bind(jsonElement, JsonRpcStrings.Testing))); - - _deserializers[typeof(ServerTestingCapabilities)] = new JsonElementDeserializer( - (json, jsonElement) => new ServerTestingCapabilities( - SupportsDiscovery: json.Bind(jsonElement, JsonRpcStrings.SupportsDiscovery), - MultiRequestSupport: json.Bind(jsonElement, JsonRpcStrings.MultiRequestSupport), - VSTestProviderSupport: json.Bind(jsonElement, JsonRpcStrings.VSTestProviderSupport), - SupportsAttachments: json.Bind(jsonElement, JsonRpcStrings.AttachmentsSupport), - MultiConnectionProvider: json.Bind(jsonElement, JsonRpcStrings.MultiConnectionProvider))); - - _deserializers[typeof(DiscoverRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => - { - string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); - if (!Guid.TryParse(runId, out Guid result)) - { - throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); - } - - json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); - json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); - - return new DiscoverRequestArgs( - RunId: result, - TestNodes: testNodes, - GraphFilter: graphFilter); - }); - - _deserializers[typeof(RunRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => - { - string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); - if (!Guid.TryParse(runId, out Guid result)) - { - throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); - } - - json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); - json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); - - return new RunRequestArgs( - RunId: result, - TestNodes: testNodes, - GraphFilter: graphFilter); - }); - - _deserializers[typeof(TestNode)] = new JsonElementDeserializer( - (json, properties) => - { - PropertyBag propertyBag = new(); - string uid = json.Bind(properties, JsonRpcStrings.Uid) ?? string.Empty; - string displayName = json.Bind(properties, JsonRpcStrings.DisplayName); - - if (json.TryBind(properties, out string? locationFile, "location.file")) - { - json.TryBind(properties, out int locationLineStart, "location.line-start"); - json.TryBind(properties, out int locationLineEnd, "location.line-end"); - - TestFileLocationProperty testFileLocationProperty = new( - locationFile!, - new LinePositionSpan(new LinePosition(locationLineStart, 0), new LinePosition(locationLineEnd, 0))); - propertyBag.Add(testFileLocationProperty); - } - - return new TestNode - { - Uid = new TestNodeUid(uid), - DisplayName = displayName, - Properties = propertyBag, - }; - }); - - _deserializers[typeof(CancelRequestArgs)] = new JsonElementDeserializer( - (json, jsonElement) => json.TryBind(jsonElement, out int id, JsonRpcStrings.Id) ? new CancelRequestArgs(id) : throw new MessageFormatException("id field should be an int")); - - _deserializers[typeof(ExitRequestArgs)] = new JsonElementDeserializer( - (json, jsonElement) => new ExitRequestArgs()); - - _deserializers[typeof(ErrorMessage)] = new JsonElementDeserializer( - (json, jsonElement) => - { - ValidateJsonRpcHeader(json, jsonElement); - - int id = json.Bind(jsonElement, JsonRpcStrings.Id); - JsonElement error = jsonElement.GetProperty(JsonRpcStrings.Error); - - int code = json.Bind(error, JsonRpcStrings.Code); - string message = json.Bind(error, JsonRpcStrings.Message); - - if (json.TryBind(error, out IDictionary? data, JsonRpcStrings.Data) && data?.Count == 0) - { - data = null; - } - - return new ErrorMessage( - Id: id, - ErrorCode: code, - Message: message ?? string.Empty, - Data: data); - }); + RegisterDefaultSerializers(_serializers); + RegisterDefaultDeserializers(_deserializers); // Try to add serializers passed from outside if (serializers is not null)