diff --git a/src/StreamJsonRpc/IGenericTypeArgAssist.cs b/src/StreamJsonRpc/IGenericTypeArgAssist.cs new file mode 100644 index 00000000..52c3e500 --- /dev/null +++ b/src/StreamJsonRpc/IGenericTypeArgAssist.cs @@ -0,0 +1,42 @@ +namespace StreamJsonRpc; + +/// +/// A non-generic interface with a generic method, for use with +/// so that generic contexts can be preserved and reused later in a non-generic context. +/// +internal interface IGenericTypeArgAssist +{ + /// + /// Invokes whatever functionality the implementation provides, using the specified generic type argument. + /// + /// A generic type argument, whose semantics are up to the implementation. + /// Optional state that the caller may have provided. + /// An arbitrary result, up to the implementation. + object? Invoke(object? state = null); +} + +/// +/// A non-generic interface allowing invocation of a generic method with a type argument known only at runtime. +/// +/// +internal interface IGenericTypeArgStore +{ + /// + /// Invokes the generic method. + /// + /// An implementation of . + /// Optional state that the caller may have provided. + /// An arbitrary result, up to the implementation. + object? Invoke(IGenericTypeArgAssist assist, object? state = null); +} + +/// +/// A generic implementation of +/// that can be created while in a generic context, so that a non-generic method can invoke another generic method later. +/// +/// The generic type argument that must be supplied later. +internal class GenericTypeArgStore : IGenericTypeArgStore +{ + /// + public object? Invoke(IGenericTypeArgAssist assist, object? state = null) => assist.Invoke(state); +} diff --git a/src/StreamJsonRpc/PolyTypeJsonFormatter.cs b/src/StreamJsonRpc/PolyTypeJsonFormatter.cs new file mode 100644 index 00000000..eb816c0a --- /dev/null +++ b/src/StreamJsonRpc/PolyTypeJsonFormatter.cs @@ -0,0 +1,1290 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO.Pipelines; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.Serialization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Nerdbank.Streams; +using PolyType; +using StreamJsonRpc.Protocol; +using StreamJsonRpc.Reflection; + +namespace StreamJsonRpc; + +/// +/// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . +/// This formatter is NativeAOT ready and relies on PolyType. +/// +[Experimental("PolyTypeJson")] +public partial class PolyTypeJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks +{ + private static readonly ProxyFactory ProxyFactory = ProxyFactory.NoDynamic; + + private static readonly JsonWriterOptions WriterOptions = new() { }; + + private static readonly JsonDocumentOptions DocumentOptions = new() { }; + + /// + /// The to use for the envelope and built-in types. + /// + private static readonly JsonSerializerOptions BuiltInSerializerOptions = new() + { + TypeInfoResolver = SourceGenerationContext.Default, + Converters = + { + RequestIdSTJsonConverter.Instance, + }, + }; + + /// + /// UTF-8 encoding without a preamble. + /// + private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + private static readonly JsonRpcProxyOptions DefaultRpcMarshalableProxyOptions = new JsonRpcProxyOptions(JsonRpcProxyOptions.Default) { AcceptProxyWithExtraInterfaces = true, IsFrozen = true }; + + private readonly Dictionary genericLifts = []; + + private readonly ToStringHelper serializationToStringHelper = new ToStringHelper(); + + private JsonSerializerOptions massagedUserDataSerializerOptions; + + /// + /// Retains the message currently being deserialized so that it can be disposed when we're done with it. + /// + private JsonDocument? deserializingDocument; + + /// + /// Initializes a new instance of the class. + /// + public PolyTypeJsonFormatter() + { + // Prepare for built-in behavior. + this.RegisterGenericType(); + + // Take care with any options set *here* instead of in MassageUserDataSerializerOptions, + // because any settings made only here will be erased if the user changes the JsonSerializerOptions property. + this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new() + { + }); + } + + /// + /// Gets the shape provider for user data types. + /// + public required ITypeShapeProvider TypeShapeProvider { get; init; } + + /// + public Encoding Encoding + { + get => DefaultEncoding; + set => throw new NotSupportedException(); + } + + /// + /// Gets or sets the options to use when serializing and deserializing JSON containing user data. + /// + public JsonSerializerOptions JsonSerializerOptions + { + get => this.massagedUserDataSerializerOptions; + set => this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new(value)); + } + + /// + public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) => this.Deserialize(contentBuffer, this.Encoding); + + /// + public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding encoding) + { + if (encoding is not UTF8Encoding) + { + throw new NotSupportedException("Only our default encoding is supported."); + } + + JsonDocument document = this.deserializingDocument = JsonDocument.Parse(contentBuffer, DocumentOptions); + if (document.RootElement.ValueKind != JsonValueKind.Object) + { + throw new JsonException("Expected a JSON object at the root of the message."); + } + + JsonRpcMessage message; + if (document.RootElement.TryGetProperty(Utf8Strings.method, out JsonElement methodElement)) + { + JsonRpcRequest request = new(this) + { + RequestId = ReadRequestId(), + Method = methodElement.GetString(), + JsonArguments = document.RootElement.TryGetProperty(Utf8Strings.@params, out JsonElement paramsElement) ? paramsElement : null, + TraceParent = document.RootElement.TryGetProperty(Utf8Strings.traceparent, out JsonElement traceParentElement) ? traceParentElement.GetString() : null, + TraceState = document.RootElement.TryGetProperty(Utf8Strings.tracestate, out JsonElement traceStateElement) ? traceStateElement.GetString() : null, + }; + message = request; + } + else if (document.RootElement.TryGetProperty(Utf8Strings.result, out JsonElement resultElement)) + { + JsonRpcResult result = new(this) + { + RequestId = ReadRequestId(), + JsonResult = resultElement, + }; + message = result; + } + else if (document.RootElement.TryGetProperty(Utf8Strings.error, out JsonElement errorElement)) + { + JsonRpcError error = new(this) + { + RequestId = ReadRequestId(), + Error = new JsonRpcError.ErrorDetail(this) + { + Code = (JsonRpcErrorCode)errorElement.GetProperty(Utf8Strings.code).GetInt64(), + Message = errorElement.GetProperty(Utf8Strings.message).GetString(), + JsonData = errorElement.TryGetProperty(Utf8Strings.data, out JsonElement dataElement) ? dataElement : null, + }, + }; + + message = error; + } + else + { + throw new JsonException("Expected a request, result, or error message."); + } + + message.Version = document.RootElement.TryGetProperty(Utf8Strings.jsonrpc, out JsonElement jsonRpcElement) + ? (jsonRpcElement.ValueEquals(Utf8Strings.v2_0) ? "2.0" : (jsonRpcElement.GetString() ?? throw new JsonException("Unexpected null value for jsonrpc property."))) + : "1.0"; + + if (message is IMessageWithTopLevelPropertyBag messageWithTopLevelPropertyBag) + { + messageWithTopLevelPropertyBag.TopLevelPropertyBag = new TopLevelPropertyBag(document, this.massagedUserDataSerializerOptions); + } + + RequestId ReadRequestId() + { + return document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement) + ? idElement.Deserialize(SourceGenerationContext.Default.RequestId) + : RequestId.NotSpecified; + } + + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; + tracingCallbacks?.OnMessageDeserialized(message, document.RootElement); + + this.TryHandleSpecialIncomingMessage(message); + + return message; + } + + /// + public object GetJsonText(JsonRpcMessage message) => throw new NotSupportedException(); + + /// + public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message) + { + Requires.NotNull(message); + + using (this.TrackSerialization(message)) + { + try + { + using Utf8JsonWriter writer = new(bufferWriter, WriterOptions); + writer.WriteStartObject(); + WriteVersion(); + switch (message) + { + case Protocol.JsonRpcRequest request: + WriteId(request.RequestId); + writer.WriteString(Utf8Strings.method, request.Method); + WriteArguments(request); + if (request.TraceParent is not null) + { + writer.WriteString(Utf8Strings.traceparent, request.TraceParent); + } + + if (request.TraceState is not null) + { + writer.WriteString(Utf8Strings.tracestate, request.TraceState); + } + + break; + case Protocol.JsonRpcResult result: + WriteId(result.RequestId); + WriteResult(result); + break; + case Protocol.JsonRpcError error: + WriteId(error.RequestId); + WriteError(error); + break; + default: + throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message)); + } + + if (message is IMessageWithTopLevelPropertyBag { TopLevelPropertyBag: TopLevelPropertyBag propertyBag }) + { + propertyBag.WriteProperties(writer); + } + + writer.WriteEndObject(); + + void WriteVersion() + { + switch (message.Version) + { + case "1.0": + // The 1.0 protocol didn't include the version property at all. + break; + case "2.0": + writer.WriteString(Utf8Strings.jsonrpc, Utf8Strings.v2_0); + break; + default: + writer.WriteString(Utf8Strings.jsonrpc, message.Version); + break; + } + } + + void WriteId(RequestId id) + { + if (!id.IsEmpty) + { + writer.WritePropertyName(Utf8Strings.id); + RequestIdSTJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); + } + } + + void WriteArguments(Protocol.JsonRpcRequest request) + { + if (request.ArgumentsList is not null) + { + writer.WriteStartArray(Utf8Strings.@params); + for (int i = 0; i < request.ArgumentsList.Count; i++) + { + WriteUserData(request.ArgumentsList[i], request.ArgumentListDeclaredTypes?[i]); + } + + writer.WriteEndArray(); + } + else if (request.NamedArguments is not null) + { + writer.WriteStartObject(Utf8Strings.@params); + foreach (KeyValuePair argument in request.NamedArguments) + { + writer.WritePropertyName(argument.Key); + WriteUserData(argument.Value, request.NamedArgumentDeclaredTypes?[argument.Key]); + } + + writer.WriteEndObject(); + } + else if (request.Arguments is not null) + { + // This is a custom named arguments object, so we'll just serialize it as-is. + writer.WritePropertyName(Utf8Strings.@params); + WriteUserData(request.Arguments, declaredType: null); + } + } + + void WriteResult(Protocol.JsonRpcResult result) + { + writer.WritePropertyName(Utf8Strings.result); + WriteUserData(result.Result, result.ResultDeclaredType); + } + + void WriteError(Protocol.JsonRpcError error) + { + if (error.Error is null) + { + throw new ArgumentException($"{nameof(error.Error)} property must be set.", nameof(message)); + } + + writer.WriteStartObject(Utf8Strings.error); + writer.WriteNumber(Utf8Strings.code, (int)error.Error.Code); + writer.WriteString(Utf8Strings.message, error.Error.Message); + if (error.Error.Data is not null) + { + writer.WritePropertyName(Utf8Strings.data); + WriteUserData(error.Error.Data, null); + } + + writer.WriteEndObject(); + } + + void WriteUserData(object? value, Type? declaredType) + { + if (value is null) + { + writer.WriteNullValue(); + } + else if (declaredType is not null && declaredType != typeof(void) && declaredType != typeof(object)) + { + JsonTypeInfo typeInfo = this.GetTypeInfoFromBuiltInOrUser(declaredType); + JsonSerializer.Serialize(writer, value, typeInfo); + } + else + { + Type normalizedType = NormalizeType(value.GetType()); + JsonTypeInfo typeInfo = this.GetTypeInfoFromBuiltInOrUser(normalizedType); + JsonSerializer.Serialize(writer, value, typeInfo); + } + } + } + catch (Exception ex) + { + throw new JsonException(Resources.SerializationFailure, ex); + } + } + } + + /// + /// Registers a type that may be used as a generic type argument for some generic value to be serialized, + /// such as or . + /// + /// The type argument to some generic type. + public void RegisterGenericType() + { + this.ThrowIfInitialized(); + if (this.genericLifts.ContainsKey(typeof(T))) + { + return; + } + + this.genericLifts.Add(typeof(T), new GenericTypeArgStore()); + } + + void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage message, ReadOnlySequence encodedMessage) + { + IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc; + this.serializationToStringHelper.Activate(encodedMessage); + try + { + tracingCallbacks?.OnMessageSerialized(message, this.serializationToStringHelper); + } + finally + { + this.serializationToStringHelper.Deactivate(); + } + } + + Protocol.JsonRpcRequest IJsonRpcMessageFactory.CreateRequestMessage() => new JsonRpcRequest(this); + + Protocol.JsonRpcError IJsonRpcMessageFactory.CreateErrorMessage() => new JsonRpcError(this); + + Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this); + + /// + private protected override MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc) => new MessageFormatterRpcMarshaledContextTracker.PolyTypeShape(rpc, ProxyFactory, this, this.TypeShapeProvider); + + private JsonTypeInfo GetTypeInfoFromBuiltInOrUser(Type type) => BuiltInSerializerOptions.TryGetTypeInfo(type, out JsonTypeInfo? typeInfo) ? typeInfo : this.massagedUserDataSerializerOptions.GetTypeInfo(type); + + private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOptions options) + { + // This is required for $/cancelRequest messages. + options.Converters.Add(RequestIdSTJsonConverter.Instance); + + // Add support for exotic types. + options.Converters.Add(new ProgressConverterFactory(this)); + options.Converters.Add(new AsyncEnumerableConverter(this)); + options.Converters.Add(new RpcMarshalableConverterFactory(this)); + options.Converters.Add(new DuplexPipeConverter(this)); + options.Converters.Add(new PipeReaderConverter(this)); + options.Converters.Add(new PipeWriterConverter(this)); + options.Converters.Add(new StreamConverter(this)); + + // Add support for serializing exceptions. + options.Converters.Add(new ExceptionConverter(this)); + + return options; + } + + private object? GenericMethodInvoke(Type typeArg, IGenericTypeArgAssist assist, object? state = null) + { + if (!this.genericLifts.TryGetValue(typeArg, out IGenericTypeArgStore? lift)) + { + throw new NotImplementedException($"{nameof(this.RegisterGenericType)}() must be called first with type argument: {typeArg}."); + } + + return lift.Invoke(assist, state); + } + + private static class Utf8Strings + { +#pragma warning disable SA1300 // Element should begin with upper-case letter + internal static ReadOnlySpan jsonrpc => "jsonrpc"u8; + + internal static ReadOnlySpan v2_0 => "2.0"u8; + + internal static ReadOnlySpan id => "id"u8; + + internal static ReadOnlySpan method => "method"u8; + + internal static ReadOnlySpan @params => "params"u8; + + internal static ReadOnlySpan traceparent => "traceparent"u8; + + internal static ReadOnlySpan tracestate => "tracestate"u8; + + internal static ReadOnlySpan result => "result"u8; + + internal static ReadOnlySpan error => "error"u8; + + internal static ReadOnlySpan code => "code"u8; + + internal static ReadOnlySpan message => "message"u8; + + internal static ReadOnlySpan data => "data"u8; +#pragma warning restore SA1300 // Element should begin with upper-case letter + } + + private class TopLevelPropertyBag : TopLevelPropertyBagBase + { + private readonly JsonDocument? incomingMessage; + private readonly JsonSerializerOptions jsonSerializerOptions; + + /// + /// Initializes a new instance of the class + /// for use with an incoming message. + /// + /// The incoming message. + /// The serializer options to use. + internal TopLevelPropertyBag(JsonDocument incomingMessage, JsonSerializerOptions jsonSerializerOptions) + : base(isOutbound: false) + { + this.incomingMessage = incomingMessage; + this.jsonSerializerOptions = jsonSerializerOptions; + } + + /// + /// Initializes a new instance of the class + /// for use with an outcoming message. + /// + /// The serializer options to use. + internal TopLevelPropertyBag(JsonSerializerOptions jsonSerializerOptions) + : base(isOutbound: true) + { + this.jsonSerializerOptions = jsonSerializerOptions; + } + + internal void WriteProperties(Utf8JsonWriter writer) + { + if (this.incomingMessage is not null) + { + // We're actually re-transmitting an incoming message (remote target feature). + // We need to copy all the properties that were in the original message. + // Don't implement this without enabling the tests for the scenario found in JsonRpcRemoteTargetPolyTypeJsonFormatterTests.cs. + // The tests fail for reasons even without this support, so there's work to do beyond just implementing this. + throw new NotImplementedException(); + } + else + { + foreach (KeyValuePair property in this.OutboundProperties) + { + writer.WritePropertyName(property.Key); + JsonTypeInfo typeInfo = this.jsonSerializerOptions.GetTypeInfo(property.Value.DeclaredType); + JsonSerializer.Serialize(writer, property.Value.Value, typeInfo); + } + } + } + + protected internal override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value) + { + if (this.incomingMessage?.RootElement.TryGetProperty(name, out JsonElement serializedValue) is true) + { + value = serializedValue.Deserialize((JsonTypeInfo)this.jsonSerializerOptions.GetTypeInfo(typeof(T))); + return true; + } + + value = default; + return false; + } + } + + private class JsonRpcRequest : JsonRpcRequestBase + { + private readonly PolyTypeJsonFormatter formatter; + + private int? argumentCount; + + private JsonElement? jsonArguments; + + internal JsonRpcRequest(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override int ArgumentCount => this.argumentCount ?? base.ArgumentCount; + + public override IEnumerable? ArgumentNames + { + get + { + return this.JsonArguments?.ValueKind is JsonValueKind.Object + ? this.JsonArguments.Value.EnumerateObject().Select(p => p.Name) + : null; + } + } + + internal JsonElement? JsonArguments + { + get => this.jsonArguments; + init + { + this.jsonArguments = value; + if (value.HasValue) + { + this.argumentCount = CountArguments(value.Value); + } + } + } + + public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span typedArguments) + { + using (this.formatter.TrackDeserialization(this, parameters)) + { + // Support for opt-in to deserializing all named arguments into a single parameter. + if (parameters.Length == 1 && this.formatter.ApplicableMethodAttributeOnDeserializingMethod?.UseSingleObjectParameterDeserialization is true && this.JsonArguments is not null) + { + typedArguments[0] = this.JsonArguments.Value.Deserialize(this.formatter.GetTypeInfoFromBuiltInOrUser(parameters[0].ParameterType)); + return ArgumentMatchResult.Success; + } + + return base.TryGetTypedArguments(parameters, typedArguments); + } + } + + public override bool TryGetArgumentByNameOrIndex(string? name, int position, Type? typeHint, out object? value) + { + if (this.JsonArguments is null) + { + value = null; + return false; + } + + JsonElement? valueElement = null; + switch (this.JsonArguments?.ValueKind) + { + case JsonValueKind.Object when name is not null: + if (this.JsonArguments.Value.TryGetProperty(name, out JsonElement propertyValue)) + { + valueElement = propertyValue; + } + + break; + case JsonValueKind.Array when position >= 0: + int elementIndex = 0; + foreach (JsonElement arrayElement in this.JsonArguments.Value.EnumerateArray()) + { + if (elementIndex++ == position) + { + valueElement = arrayElement; + break; + } + } + + break; + } + + try + { + using (this.formatter.TrackDeserialization(this)) + { + try + { + value = valueElement?.Deserialize(this.formatter.GetTypeInfoFromBuiltInOrUser(typeHint ?? typeof(object))); + } + catch (Exception ex) + { + if (this.formatter.JsonRpc?.TraceSource.Switch.ShouldTrace(TraceEventType.Warning) ?? false) + { + this.formatter.JsonRpc.TraceSource.TraceEvent(TraceEventType.Warning, (int)JsonRpc.TraceEvents.MethodArgumentDeserializationFailure, Resources.FailureDeserializingRpcArgument, name, position, typeHint, ex); + } + + throw new RpcArgumentDeserializationException(name, position, typeHint, ex); + } + } + } + catch (JsonException ex) + { + throw new RpcArgumentDeserializationException(name, position, typeHint, ex); + } + + return valueElement.HasValue; + } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + this.jsonArguments = null; + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; + } + + private static int CountArguments(JsonElement arguments) + { + int count; + switch (arguments.ValueKind) + { + case JsonValueKind.Array: + count = arguments.GetArrayLength(); + + break; + case JsonValueKind.Object: + count = 0; + foreach (JsonProperty property in arguments.EnumerateObject()) + { + count++; + } + + break; + default: + throw new InvalidOperationException("Unexpected value kind: " + arguments.ValueKind); + } + + return count; + } + } + + private class JsonRpcResult : JsonRpcResultBase + { + private readonly PolyTypeJsonFormatter formatter; + + private Exception? resultDeserializationException; + + internal JsonRpcResult(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + internal JsonElement? JsonResult { get; set; } + + public override T GetResult() + { + if (this.resultDeserializationException is not null) + { + ExceptionDispatchInfo.Capture(this.resultDeserializationException).Throw(); + } + + return this.JsonResult is null + ? (T)this.Result! + : this.JsonResult.Value.Deserialize((JsonTypeInfo)this.formatter.massagedUserDataSerializerOptions.GetTypeInfo(typeof(T)))!; + } + + protected internal override void SetExpectedResultType(Type resultType) + { + Verify.Operation(this.JsonResult is not null, "Result is no longer available or has already been deserialized."); + + try + { + using (this.formatter.TrackDeserialization(this)) + { + this.Result = this.JsonResult.Value.Deserialize(this.formatter.massagedUserDataSerializerOptions.GetTypeInfo(resultType)); + } + + this.JsonResult = default; + } + catch (Exception ex) + { + // This was a best effort anyway. We'll throw again later at a more convenient time for JsonRpc. + this.resultDeserializationException = new JsonException(string.Format(CultureInfo.CurrentCulture, Resources.FailureDeserializingRpcResult, resultType.Name, ex.GetType().Name, ex.Message), ex); + } + } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + this.JsonResult = null; + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; + } + } + + private class JsonRpcError : JsonRpcErrorBase + { + private readonly PolyTypeJsonFormatter formatter; + + public JsonRpcError(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + internal new ErrorDetail? Error + { + get => (ErrorDetail?)base.Error; + set => base.Error = value; + } + + protected override TopLevelPropertyBagBase? CreateTopLevelPropertyBag() => new TopLevelPropertyBag(this.formatter.massagedUserDataSerializerOptions); + + protected override void ReleaseBuffers() + { + base.ReleaseBuffers(); + if (this.Error is { } detail) + { + detail.JsonData = null; + } + + this.formatter.deserializingDocument?.Dispose(); + this.formatter.deserializingDocument = null; + } + + internal new class ErrorDetail : Protocol.JsonRpcError.ErrorDetail + { + private readonly PolyTypeJsonFormatter formatter; + + internal ErrorDetail(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + internal JsonElement? JsonData { get; set; } + + public override object? GetData(Type dataType) + { + Requires.NotNull(dataType, nameof(dataType)); + if (this.JsonData is null) + { + return this.Data; + } + + try + { + return this.JsonData.Value.Deserialize(this.formatter.GetTypeInfoFromBuiltInOrUser(dataType)); + } + catch (JsonException) + { + // Deserialization failed. Try returning array/dictionary based primitive objects. + try + { + return this.JsonData.Value.Deserialize(SourceGenerationContext.Default.Object); + } + catch (JsonException) + { + return null; + } + } + } + + protected internal override void SetExpectedDataType(Type dataType) + { + this.Data = this.GetData(dataType); + + // Clear the source now that we've deserialized to prevent GetData from attempting + // deserialization later when the buffer may be recycled on another thread. + this.JsonData = default; + } + } + } + + private class ProgressConverterFactory : JsonConverterFactory, IGenericTypeArgAssist + { + private readonly PolyTypeJsonFormatter formatter; + + internal ProgressConverterFactory(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) => TrackerHelpers.FindIProgressInterfaceImplementedBy(typeToConvert) is not null; + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type? iface = TrackerHelpers.FindIProgressInterfaceImplementedBy(typeToConvert); + Assumes.NotNull(iface); + return (JsonConverter)this.formatter.GenericMethodInvoke(iface.GetGenericArguments()[0], this)!; + } + + object IGenericTypeArgAssist.Invoke(object? state) => new Converter(this.formatter); + + private class Converter : JsonConverter> + { + private readonly PolyTypeJsonFormatter formatter; + + public Converter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override IProgress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Assumes.NotNull(this.formatter.JsonRpc); + object token = reader.TokenType switch + { + JsonTokenType.String => reader.GetString()!, + JsonTokenType.Number => reader.GetInt64(), + _ => throw new NotSupportedException("Unsupported token type."), // Ideally, we should *copy* the token so we can retain it and replay it later. + }; + + bool clientRequiresNamedArgs = this.formatter.ApplicableMethodAttributeOnDeserializingMethod is { ClientRequiresNamedArguments: true }; + return (IProgress)this.formatter.FormatterProgressTracker.CreateProgress(this.formatter.JsonRpc, token, clientRequiresNamedArgs); + } + + public override void Write(Utf8JsonWriter writer, IProgress value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.FormatterProgressTracker.GetTokenForProgress(value)); + } + } + } + + private class AsyncEnumerableConverter : JsonConverterFactory, IGenericTypeArgAssist + { + private readonly PolyTypeJsonFormatter formatter; + + internal AsyncEnumerableConverter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) => TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(typeToConvert) is not null; + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type? iface = TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(typeToConvert); + Assumes.NotNull(iface); + return (JsonConverter)this.formatter.GenericMethodInvoke(iface.GetGenericArguments()[0], this)!; + } + + object? IGenericTypeArgAssist.Invoke(object? state) => new Converter(this.formatter); + + private class Converter : JsonConverter> + { + private readonly PolyTypeJsonFormatter formatter; + + public Converter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override IAsyncEnumerable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using JsonDocument wrapper = JsonDocument.ParseValue(ref reader); + JsonElement? handle = null; + if (wrapper.RootElement.TryGetProperty(MessageFormatterEnumerableTracker.TokenPropertyName, out JsonElement enumToken)) + { + // Copy the token so we can retain it and replay it later. + handle = enumToken.Clone(); + } + + IReadOnlyList? prefetchedItems = null; + if (wrapper.RootElement.TryGetProperty(MessageFormatterEnumerableTracker.ValuesPropertyName, out JsonElement prefetchedElement)) + { + prefetchedItems = prefetchedElement.Deserialize((JsonTypeInfo>)options.GetTypeInfo(typeof(IReadOnlyList))); + } + + return this.formatter.EnumerableTracker.CreateEnumerableProxy(handle, prefetchedItems); + } + + public override void Write(Utf8JsonWriter writer, IAsyncEnumerable value, JsonSerializerOptions options) + { + (IReadOnlyList Elements, bool Finished) prefetched = value.TearOffPrefetchedElements(); + long token = this.formatter.EnumerableTracker.GetToken(value); + writer.WriteStartObject(); + if (!prefetched.Finished) + { + writer.WriteNumber(MessageFormatterEnumerableTracker.TokenPropertyName, token); + } + + if (prefetched.Elements.Count > 0) + { + writer.WritePropertyName(MessageFormatterEnumerableTracker.ValuesPropertyName); + JsonSerializer.Serialize(writer, prefetched.Elements, options.GetTypeInfo(typeof(IReadOnlyList))); + } + + writer.WriteEndObject(); + } + } + } + + private class RpcMarshalableConverterFactory : JsonConverterFactory, IGenericTypeArgAssist + { + private readonly PolyTypeJsonFormatter formatter; + + public RpcMarshalableConverterFactory(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) => MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(this.formatter.TypeShapeProvider.GetTypeShapeOrThrow(typeToConvert), DefaultRpcMarshalableProxyOptions, out _, out _, out _); + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return (JsonConverter)this.formatter.GenericMethodInvoke(typeToConvert, this)!; + } + + object? IGenericTypeArgAssist.Invoke(object? state) => new Converter(this.formatter); + + private class Converter : JsonConverter + { + private readonly PolyTypeJsonFormatter formatter; + private readonly JsonRpcProxyOptions proxyOptions; + private readonly JsonRpcTargetOptions targetOptions; + private readonly RpcMarshalableAttribute rpcMarshalableAttribute; + + public Converter(PolyTypeJsonFormatter formatter) + { + Assumes.True(MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType( + formatter.TypeShapeProvider.GetTypeShapeOrThrow(typeof(T)), + DefaultRpcMarshalableProxyOptions, + out JsonRpcProxyOptions? proxyOptions, + out JsonRpcTargetOptions? targetOptions, + out RpcMarshalableAttribute? attribute)); + + this.formatter = formatter; + this.proxyOptions = proxyOptions; + this.targetOptions = targetOptions; + this.rpcMarshalableAttribute = attribute; + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = JsonSerializer.Deserialize(ref reader, SourceGenerationContext.Default.MarshalToken); + return (T)this.formatter.RpcMarshaledContextTracker.GetObject(typeof(T), token, this.proxyOptions); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + RpcTargetMetadata mapping = RpcTargetMetadata.FromShape(this.formatter.TypeShapeProvider.GetTypeShapeOrThrow(typeof(T))); + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = this.formatter.RpcMarshaledContextTracker.GetToken(value!, this.targetOptions, mapping, this.rpcMarshalableAttribute); + JsonSerializer.Serialize(writer, token, SourceGenerationContext.Default.MarshalToken); + } + } + } + + private class DuplexPipeConverter : JsonConverter + { + private readonly PolyTypeJsonFormatter formatter; + + internal DuplexPipeConverter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(IDuplexPipe).IsAssignableFrom(typeToConvert); + + public override IDuplexPipe Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return this.formatter.DuplexPipeTracker.GetPipe(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, IDuplexPipe value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); + } + } + + private class PipeReaderConverter : JsonConverter + { + private readonly PolyTypeJsonFormatter formatter; + + internal PipeReaderConverter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(PipeReader).IsAssignableFrom(typeToConvert); + + public override PipeReader Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return this.formatter.DuplexPipeTracker!.GetPipeReader(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, PipeReader value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); + } + } + + private class PipeWriterConverter : JsonConverter + { + private readonly PolyTypeJsonFormatter formatter; + + internal PipeWriterConverter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(PipeWriter).IsAssignableFrom(typeToConvert); + + public override PipeWriter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return this.formatter.DuplexPipeTracker.GetPipeWriter(reader.GetUInt64()); + } + + public override void Write(Utf8JsonWriter writer, PipeWriter value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value).Value); + } + } + + private class StreamConverter : JsonConverter + { + private readonly PolyTypeJsonFormatter formatter; + + internal StreamConverter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public override bool CanConvert(Type typeToConvert) => typeof(Stream).IsAssignableFrom(typeToConvert); + + public override Stream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return this.formatter.DuplexPipeTracker.GetPipe(reader.GetUInt64()).AsStream(); + } + + public override void Write(Utf8JsonWriter writer, Stream value, JsonSerializerOptions options) + { + writer.WriteNumberValue(this.formatter.DuplexPipeTracker.GetULongToken(value.UsePipe()).Value); + } + } + + private class ExceptionConverter : JsonConverter + { + /// + /// Tracks recursion count while serializing or deserializing an exception. + /// + private static readonly ThreadLocal ExceptionRecursionCounter = new(); + + private readonly PolyTypeJsonFormatter formatter; + + internal ExceptionConverter(PolyTypeJsonFormatter formatter) + { + this.formatter = formatter; + } + + public override bool CanConvert(Type typeToConvert) => typeof(Exception).IsAssignableFrom(typeToConvert); + + public override Exception? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Assumes.NotNull(this.formatter.JsonRpc); + + ExceptionRecursionCounter.Value++; + try + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new InvalidOperationException("Expected a StartObject token."); + } + + if (ExceptionRecursionCounter.Value > this.formatter.JsonRpc.ExceptionOptions.RecursionLimit) + { + // Exception recursion has gone too deep. Skip this value and return null as if there were no inner exception. + // Note that in skipping, the parser may use recursion internally and may still throw if its own limits are exceeded. + reader.Skip(); + return null; + } + + JsonNode? jsonNode = JsonNode.Parse(ref reader) ?? throw new JsonException("Unexpected null"); + SerializationInfo? info = new SerializationInfo(typeToConvert, new JsonConverterFormatter(this.formatter.massagedUserDataSerializerOptions)); + foreach (KeyValuePair property in jsonNode.AsObject()) + { + info.AddSafeValue(property.Key, property.Value); + } + + return ExceptionSerializationHelpers.Deserialize(this.formatter.JsonRpc, info, this.formatter.JsonRpc, this.formatter.JsonRpc?.TraceSource); + } + finally + { + ExceptionRecursionCounter.Value--; + } + } + + public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) + { + // We have to guard our own recursion because the serializer has no visibility into inner exceptions. + // Each exception in the russian doll is a new serialization job from its perspective. + ExceptionRecursionCounter.Value++; + try + { + if (ExceptionRecursionCounter.Value > this.formatter.JsonRpc?.ExceptionOptions.RecursionLimit) + { + // Exception recursion has gone too deep. Skip this value and write null as if there were no inner exception. + writer.WriteNullValue(); + return; + } + + SerializationInfo info = new SerializationInfo(value.GetType(), new JsonConverterFormatter(this.formatter.massagedUserDataSerializerOptions)); + ExceptionSerializationHelpers.Serialize(value, info); + writer.WriteStartObject(); + foreach (SerializationEntry element in info.GetSafeMembers()) + { + writer.WritePropertyName(element.Name); + if (element.Value is null) + { + writer.WriteNullValue(); + } + else if (element.ObjectType == typeof(System.Collections.IDictionary)) + { + // Some exception types tuck data into this dictionary that is assumed to be safe to read back (e.g. use a BinaryFormatter to interpret its data). + // Also, it's difficult to safely deserialize an untyped dictionary because we don't have an hash-collision resistant key comparer for System.Object. + // So just skip it. + writer.WriteNullValue(); + } + else + { + // We prefer the declared type but will fallback to the runtime type. + Type preferredType = NormalizeType(element.ObjectType); + Type fallbackType = NormalizeType(element.Value.GetType()); + if (!this.formatter.massagedUserDataSerializerOptions.TryGetTypeInfo(preferredType, out JsonTypeInfo? typeInfo) && + !this.formatter.massagedUserDataSerializerOptions.TryGetTypeInfo(fallbackType, out typeInfo)) + { + throw new NotSupportedException($"Unable to find JsonTypeInfo for {preferredType} or {fallbackType}."); + } + + JsonSerializer.Serialize(writer, element.Value, typeInfo); + } + } + + writer.WriteEndObject(); + } + catch (Exception ex) + { + throw new JsonException(ex.Message, ex); + } + finally + { + ExceptionRecursionCounter.Value--; + } + } + } + + private class JsonConverterFormatter : IFormatterConverter + { + private readonly JsonSerializerOptions serializerOptions; + + internal JsonConverterFormatter(JsonSerializerOptions serializerOptions) + { + this.serializerOptions = serializerOptions; + } + +#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 + { + var jsonValue = (JsonNode)value; + + if (type == typeof(System.Collections.IDictionary)) + { + // In this world, we may in fact be returning a null value based on a non-null value. + return DeserializePrimitive(jsonValue); + } + + return jsonValue.Deserialize(this.serializerOptions.GetTypeInfo(type))!; + } + + public object Convert(object value, TypeCode typeCode) + { + return typeCode switch + { + TypeCode.Object => ((JsonNode)value).Deserialize(this.serializerOptions.GetTypeInfo(typeof(object)))!, + _ => ExceptionSerializationHelpers.Convert(this, value, typeCode), + }; + } + + public bool ToBoolean(object value) => ((JsonNode)value).GetValue(); + + public byte ToByte(object value) => ((JsonNode)value).GetValue(); + + public char ToChar(object value) => ((JsonNode)value).GetValue(); + + public DateTime ToDateTime(object value) => ((JsonNode)value).GetValue(); + + public decimal ToDecimal(object value) => ((JsonNode)value).GetValue(); + + public double ToDouble(object value) => ((JsonNode)value).GetValue(); + + public short ToInt16(object value) => ((JsonNode)value).GetValue(); + + public int ToInt32(object value) => ((JsonNode)value).GetValue(); + + public long ToInt64(object value) => ((JsonNode)value).GetValue(); + + public sbyte ToSByte(object value) => ((JsonNode)value).GetValue(); + + public float ToSingle(object value) => ((JsonNode)value).GetValue(); + + public string? ToString(object value) => ((JsonNode)value).GetValue(); + + public ushort ToUInt16(object value) => ((JsonNode)value).GetValue(); + + public uint ToUInt32(object value) => ((JsonNode)value).GetValue(); + + public ulong ToUInt64(object value) => ((JsonNode)value).GetValue(); + + private static object? DeserializePrimitive(JsonNode? node) + { + return node switch + { + JsonObject o => DeserializeObjectAsDictionary(o), + JsonValue v => DeserializePrimitive(v.GetValue()), + JsonArray a => a.Select(DeserializePrimitive).ToArray(), + null => null, + _ => throw new NotSupportedException("Unrecognized node type: " + node.GetType().Name), + }; + } + + private static Dictionary DeserializeObjectAsDictionary(JsonNode jsonNode) + { + Dictionary dictionary = new(); + foreach (KeyValuePair property in jsonNode.AsObject()) + { + dictionary.Add(property.Key, DeserializePrimitive(property.Value)); + } + + return dictionary; + } + + private static object? DeserializePrimitive(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt32(out int intValue) ? intValue : element.GetInt64(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => throw new NotSupportedException(), + }; + } + } + + /// + private class ToStringHelper + { + private ReadOnlySequence? encodedMessage; + private string? jsonString; + + public override string ToString() + { + Verify.Operation(this.encodedMessage.HasValue, "This object has not been activated. It may have already been recycled."); + + using JsonDocument doc = JsonDocument.Parse(this.encodedMessage.Value); + return this.jsonString ??= doc.RootElement.ToString(); + } + + /// + /// Initializes this object to represent a message. + /// + internal void Activate(ReadOnlySequence encodedMessage) + { + this.encodedMessage = encodedMessage; + } + + /// + /// Cleans out this object to release memory and ensure throws if someone uses it after deactivation. + /// + internal void Deactivate() + { + this.encodedMessage = null; + this.jsonString = null; + } + } +} diff --git a/src/StreamJsonRpc/RequestId.cs b/src/StreamJsonRpc/RequestId.cs index eda0a62b..25c9c891 100644 --- a/src/StreamJsonRpc/RequestId.cs +++ b/src/StreamJsonRpc/RequestId.cs @@ -12,6 +12,7 @@ namespace StreamJsonRpc; /// Represents the ID of a request, whether it is a number or a string. /// [JsonConverter(typeof(RequestIdJsonConverter))] +[System.Text.Json.Serialization.JsonConverter(typeof(RequestIdSTJsonConverter))] public struct RequestId : IEquatable { /// diff --git a/src/StreamJsonRpc/RequestIdSTJsonConverter.cs b/src/StreamJsonRpc/RequestIdSTJsonConverter.cs new file mode 100644 index 00000000..ab668a9e --- /dev/null +++ b/src/StreamJsonRpc/RequestIdSTJsonConverter.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StreamJsonRpc; + +/// +/// A System.Text.Json converter for . +/// +internal class RequestIdSTJsonConverter : JsonConverter +{ + /// + /// A singleton that can be used to reduce allocations. + /// + internal static readonly RequestIdSTJsonConverter Instance = new(); + + /// + public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => new RequestId(reader.GetInt64()), + JsonTokenType.String => new RequestId(reader.GetString()), + JsonTokenType.Null => RequestId.Null, + _ => throw new JsonException("Unexpected token type for id property: " + reader.TokenType), + }; + } + + /// + public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options) + { + if (value.Number is long idNumber) + { + writer.WriteNumberValue(idNumber); + } + else if (value.String is string idString) + { + writer.WriteStringValue(idString); + } + else + { + writer.WriteNullValue(); + } + } +} diff --git a/src/StreamJsonRpc/SourceGenerationContext.cs b/src/StreamJsonRpc/SourceGenerationContext.cs new file mode 100644 index 00000000..8425b2c1 --- /dev/null +++ b/src/StreamJsonRpc/SourceGenerationContext.cs @@ -0,0 +1,17 @@ +// 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.Serialization; +using StreamJsonRpc.Protocol; +using StreamJsonRpc.Reflection; + +namespace StreamJsonRpc; + +/// +/// System.Text.Json source generation context for StreamJsonRpc types. +/// +[JsonSerializable(typeof(RequestId))] +[JsonSerializable(typeof(MessageFormatterRpcMarshaledContextTracker.MarshalToken))] +[JsonSerializable(typeof(object))] +[JsonSerializable(typeof(CommonErrorData))] +internal partial class SourceGenerationContext : JsonSerializerContext; diff --git a/src/StreamJsonRpc/StreamJsonRpc.csproj b/src/StreamJsonRpc/StreamJsonRpc.csproj index 6b59dc0c..4a278bff 100644 --- a/src/StreamJsonRpc/StreamJsonRpc.csproj +++ b/src/StreamJsonRpc/StreamJsonRpc.csproj @@ -23,14 +23,14 @@ - + - + @@ -50,6 +50,6 @@ - + diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 652d5974..16b62eed 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; -using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -14,7 +13,6 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using Nerdbank.Streams; using StreamJsonRpc.Protocol; using StreamJsonRpc.Reflection; @@ -41,7 +39,7 @@ public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFor TypeInfoResolver = SourceGenerationContext.Default, Converters = { - RequestIdJsonConverter.Instance, + RequestIdSTJsonConverter.Instance, }, }; @@ -248,7 +246,7 @@ void WriteId(RequestId id) if (!id.IsEmpty) { writer.WritePropertyName(Utf8Strings.id); - RequestIdJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); + RequestIdSTJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions); } } @@ -353,7 +351,7 @@ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage me private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOptions options) { // This is required for $/cancelRequest messages. - options.Converters.Add(RequestIdJsonConverter.Instance); + options.Converters.Add(RequestIdSTJsonConverter.Instance); // Add support for exotic types. options.Converters.Add(new ProgressConverterFactory(this)); @@ -746,38 +744,6 @@ protected internal override void SetExpectedDataType(Type dataType) } } - private class RequestIdJsonConverter : JsonConverter - { - internal static readonly RequestIdJsonConverter Instance = new(); - - public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TokenType switch - { - JsonTokenType.Number => new RequestId(reader.GetInt64()), - JsonTokenType.String => new RequestId(reader.GetString()), - JsonTokenType.Null => RequestId.Null, - _ => throw new JsonException("Unexpected token type for id property: " + reader.TokenType), - }; - } - - public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options) - { - if (value.Number is long idNumber) - { - writer.WriteNumberValue(idNumber); - } - else if (value.String is string idString) - { - writer.WriteStringValue(idString); - } - else - { - writer.WriteNullValue(); - } - } - } - [RequiresDynamicCode(RuntimeReasons.Formatters)] private class ProgressConverterFactory : JsonConverterFactory { @@ -1223,135 +1189,6 @@ public object Convert(object value, TypeCode typeCode) } } - /// - /// Adds compatibility with DataContractSerializer attributes. - /// - /// - /// To enable this resolver, add the following when creating your instance: - /// - /// - [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] - private class DataContractResolver : IJsonTypeInfoResolver - { - private readonly ConcurrentDictionary typeInfoCache = new(); - - /// - /// Initializes a new instance of the class. - /// - public DataContractResolver() - { - } - - /// - /// Gets the fallback resolver to use for types lacking a . - /// - /// The default value is an instance of . - public IJsonTypeInfoResolver FallbackResolver { get; init; } = new DefaultJsonTypeInfoResolver(); - - /// - public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) - { - if (!this.typeInfoCache.TryGetValue(type, out JsonTypeInfo? typeInfo)) - { - DataContractAttribute? dataContractAttribute = type.GetCustomAttribute(); - if (dataContractAttribute is not null) - { - // PERF: Consider using the generic CreateJsonTypeInfo method to avoid boxing. - // But even so, the JsonPropertyInfo generic type is internal, so we can't avoid boxing of property/field values. - typeInfo = JsonTypeInfo.CreateJsonTypeInfo(type, options); - typeInfo.CreateObject = () => FormatterServices.GetUninitializedObject(type); - PopulateMembersInfos(type, typeInfo, dataContractAttribute); - } - else - { - typeInfo = this.FallbackResolver.GetTypeInfo(type, options); - } - - this.typeInfoCache.TryAdd(type, typeInfo); - } - - return typeInfo; - } - - private static void PopulateMembersInfos( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicProperties)] Type type, - JsonTypeInfo jsonTypeInfo, - DataContractAttribute? dataContractAttribute) - { - BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; - - // When the type is decorated with DataContractAttribute, we can consider non-public members. - if (dataContractAttribute is not null) - { - bindingFlags |= BindingFlags.NonPublic; - } - - foreach (PropertyInfo propertyInfo in type.GetProperties(bindingFlags)) - { - if (TryCreateJsonPropertyInfo(propertyInfo, propertyInfo.PropertyType, out JsonPropertyInfo? jsonPropertyInfo)) - { - if (propertyInfo.CanRead) - { - jsonPropertyInfo.Get = propertyInfo.GetValue; - } - - if (propertyInfo.CanWrite) - { - jsonPropertyInfo.Set = propertyInfo.SetValue; - } - } - } - - foreach (FieldInfo fieldInfo in type.GetFields(bindingFlags)) - { - if (TryCreateJsonPropertyInfo(fieldInfo, fieldInfo.FieldType, out JsonPropertyInfo? jsonPropertyInfo)) - { - jsonPropertyInfo.Get = fieldInfo.GetValue; - if (!fieldInfo.IsInitOnly) - { - jsonPropertyInfo.Set = fieldInfo.SetValue; - } - } - } - - bool TryCreateJsonPropertyInfo( - MemberInfo memberInfo, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type propertyType, - [NotNullWhen(true)] out JsonPropertyInfo? jsonPropertyInfo) - { - DataMemberAttribute? dataMemberAttribute = memberInfo.GetCustomAttribute(); - if ((dataContractAttribute is null || dataMemberAttribute is not null) && memberInfo.GetCustomAttribute() is null) - { - jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(propertyType, dataMemberAttribute?.Name ?? memberInfo.Name); - if (dataMemberAttribute is not null) - { - jsonPropertyInfo.Order = dataMemberAttribute.Order; - jsonPropertyInfo.IsRequired = dataMemberAttribute.IsRequired; - if (!dataMemberAttribute.EmitDefaultValue) - { - object? defaultValue = propertyType.IsValueType ? FormatterServices.GetUninitializedObject(propertyType) : null; - jsonPropertyInfo.ShouldSerialize = (_, value) => !object.Equals(defaultValue, value); - } - } - - jsonTypeInfo.Properties.Add(jsonPropertyInfo); - return true; - } - - jsonPropertyInfo = null; - return false; - } - } - } - /// private class ToStringHelper { @@ -1383,7 +1220,4 @@ internal void Deactivate() this.jsonString = null; } } - - [JsonSerializable(typeof(RequestId))] - private partial class SourceGenerationContext : JsonSerializerContext; } diff --git a/test/StreamJsonRpc.Tests/AsyncEnumerablePolyTypeJsonTests.cs b/test/StreamJsonRpc.Tests/AsyncEnumerablePolyTypeJsonTests.cs new file mode 100644 index 00000000..c0c6f8cc --- /dev/null +++ b/test/StreamJsonRpc.Tests/AsyncEnumerablePolyTypeJsonTests.cs @@ -0,0 +1,47 @@ +// 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 PolyTypeJson + +using System.Text.Json; +using System.Text.Json.Serialization; +using StreamJsonRpc.Reflection; + +public partial class AsyncEnumerablePolyTypeJsonTests : AsyncEnumerableTests +{ + public AsyncEnumerablePolyTypeJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override void InitializeFormattersAndHandlers() + { + static PolyTypeJsonFormatter CreateFormatter() + { + var formatter = new PolyTypeJsonFormatter + { + JsonSerializerOptions = + { + TypeInfoResolver = SourceGenerationContext4.Default, + }, + TypeShapeProvider = PolyType.SourceGenerator.TypeShapeProvider_StreamJsonRpc_Tests.Default, + }; + + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + + return formatter; + } + + this.serverMessageFormatter = CreateFormatter(); + this.clientMessageFormatter = CreateFormatter(); + } + + [JsonSerializable(typeof(MessageFormatterEnumerableTracker.EnumeratorResults))] + [JsonSerializable(typeof(MessageFormatterEnumerableTracker.EnumeratorResults))] + [JsonSerializable(typeof(CompoundEnumerableResult))] + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(IAsyncEnumerable))] + [JsonSerializable(typeof(IAsyncEnumerable))] + private partial class SourceGenerationContext4 : JsonSerializerContext; +} diff --git a/test/StreamJsonRpc.Tests/DisposableProxyPolyTypeJsonTests.cs b/test/StreamJsonRpc.Tests/DisposableProxyPolyTypeJsonTests.cs new file mode 100644 index 00000000..e1746e27 --- /dev/null +++ b/test/StreamJsonRpc.Tests/DisposableProxyPolyTypeJsonTests.cs @@ -0,0 +1,39 @@ +// 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 PolyTypeJson + +using System.Text.Json; +using System.Text.Json.Serialization; +using static JsonRpcTests; + +public partial class DisposableProxyPolyTypeJsonTests : DisposableProxyTests +{ + public DisposableProxyPolyTypeJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override Type FormatterExceptionType => typeof(JsonException); + + protected override IJsonRpcMessageFormatter CreateFormatter() + { + var formatter = new PolyTypeJsonFormatter + { + JsonSerializerOptions = + { + TypeInfoResolver = SourceGenerationContext3.Default, + }, + TypeShapeProvider = PolyType.SourceGenerator.TypeShapeProvider_StreamJsonRpc_Tests.Default, + }; + + return formatter; + } + + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(DataContainer))] + [JsonSerializable(typeof(ProxyContainer))] + [JsonSerializable(typeof(IDisposable))] + [JsonSerializable(typeof(TypeThrowsWhenSerialized))] + private partial class SourceGenerationContext3 : JsonSerializerContext; +} diff --git a/test/StreamJsonRpc.Tests/FormatterTestBase.cs b/test/StreamJsonRpc.Tests/FormatterTestBase.cs index 537938f5..30c3ebe7 100644 --- a/test/StreamJsonRpc.Tests/FormatterTestBase.cs +++ b/test/StreamJsonRpc.Tests/FormatterTestBase.cs @@ -1,7 +1,24 @@ -using System.Runtime.Serialization; +#pragma warning disable SA1402 // File may only contain a single type + +using System.Runtime.Serialization; using Nerdbank.Streams; -public abstract class FormatterTestBase : TestBase +public abstract class FormatterTestBase : TestBase +{ + protected FormatterTestBase(ITestOutputHelper logger) + : base(logger) + { + } + + [DataContract] + public class CustomType + { + [DataMember] + public int Age { get; set; } + } +} + +public abstract class FormatterTestBase : FormatterTestBase where TFormatter : IJsonRpcMessageFormatter { private TFormatter? formatter; @@ -103,11 +120,4 @@ protected T Roundtrip(T value) var actual = (T)this.Formatter.Deserialize(sequence); return actual; } - - [DataContract] - public class CustomType - { - [DataMember] - public int Age { get; set; } - } } diff --git a/test/StreamJsonRpc.Tests/JsonRpcPolyTypeJsonHeadersTests.cs b/test/StreamJsonRpc.Tests/JsonRpcPolyTypeJsonHeadersTests.cs new file mode 100644 index 00000000..54b47931 --- /dev/null +++ b/test/StreamJsonRpc.Tests/JsonRpcPolyTypeJsonHeadersTests.cs @@ -0,0 +1,144 @@ +// 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 PolyTypeJson + +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.VisualStudio.Threading; + +public partial class JsonRpcPolyTypeJsonHeadersTests : JsonRpcTests +{ + public JsonRpcPolyTypeJsonHeadersTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override Type FormatterExceptionType => typeof(JsonException); + + [Fact] + public override async Task CanPassExceptionFromServer_ErrorData() + { + RemoteInvocationException exception = await Assert.ThrowsAnyAsync(() => this.clientRpc.InvokeAsync(nameof(Server.MethodThatThrowsUnauthorizedAccessException))); + Assert.Equal((int)JsonRpcErrorCode.InvocationError, exception.ErrorCode); + + var errorData = Assert.IsType(exception.ErrorData); + Assert.NotNull(errorData.StackTrace); + Assert.StrictEqual(COR_E_UNAUTHORIZEDACCESS, errorData.HResult); + } + + protected override void InitializeFormattersAndHandlers( + Stream serverStream, + Stream clientStream, + out IJsonRpcMessageFormatter serverMessageFormatter, + out IJsonRpcMessageFormatter clientMessageFormatter, + out IJsonRpcMessageHandler serverMessageHandler, + out IJsonRpcMessageHandler clientMessageHandler, + bool controlledFlushingClient) + { + clientMessageFormatter = CreateFormatter(); + serverMessageFormatter = CreateFormatter(); + + serverMessageHandler = new HeaderDelimitedMessageHandler(serverStream, serverStream, serverMessageFormatter); + clientMessageHandler = controlledFlushingClient + ? new DelayedFlushingHandler(clientStream, clientMessageFormatter) + : new HeaderDelimitedMessageHandler(clientStream, clientStream, clientMessageFormatter); + + static PolyTypeJsonFormatter CreateFormatter() + { + PolyTypeJsonFormatter formatter = new() + { + JsonSerializerOptions = + { + TypeInfoResolver = SourceGenerationContext.Default, + }, + TypeShapeProvider = PolyType.SourceGenerator.TypeShapeProvider_StreamJsonRpc_Tests.Default, + }; + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + return formatter; + } + } + + protected override object[] CreateFormatterIntrinsicParamsObject(string arg) => + [ + new JsonObject { ["arg"] = JsonValue.Create(arg) }, + JsonDocument.Parse($$"""{ "arg": "{{arg}}" }""").RootElement, // JsonElement + ]; + + public class TypeThrowsWhenDeserializedConverter : JsonConverter + { + public override TypeThrowsWhenDeserialized? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw CreateExceptionToBeThrownByDeserializer(); + } + + public override void Write(Utf8JsonWriter writer, TypeThrowsWhenDeserialized value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + } + + protected class DelayedFlushingHandler : HeaderDelimitedMessageHandler, IControlledFlushHandler + { + public DelayedFlushingHandler(Stream stream, IJsonRpcMessageFormatter formatter) + : base(stream, formatter) + { + } + + public AsyncAutoResetEvent FlushEntered { get; } = new AsyncAutoResetEvent(); + + public AsyncManualResetEvent AllowFlushAsyncExit { get; } = new AsyncManualResetEvent(); + + protected override async ValueTask FlushAsync(CancellationToken cancellationToken) + { + this.FlushEntered.Set(); + await this.AllowFlushAsyncExit.WaitAsync(CancellationToken.None); + await base.FlushAsync(cancellationToken); + } + } + + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(int?))] + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(double))] + [JsonSerializable(typeof(Guid))] + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(JsonObject))] + [JsonSerializable(typeof(Exception))] + [JsonSerializable(typeof(ArgumentOutOfRangeException))] + [JsonSerializable(typeof(PrivateSerializableException))] + [JsonSerializable(typeof(JsonException))] + [JsonSerializable(typeof(FileNotFoundException))] + [JsonSerializable(typeof(Foo))] + [JsonSerializable(typeof(Server.CustomErrorData))] + [JsonSerializable(typeof(ExceptionMissingDeserializingConstructor))] + [JsonSerializable(typeof(TypeThrowsWhenSerialized))] + [JsonSerializable(typeof(TypeThrowsWhenDeserialized))] + [JsonSerializable(typeof(InvalidOperationException))] + [JsonSerializable(typeof(VAndWProperties))] + [JsonSerializable(typeof(XAndYProperties))] + [JsonSerializable(typeof(XAndYPropertiesWithProgress))] + [JsonSerializable(typeof(ParamsObjectWithCustomNames))] + [JsonSerializable(typeof(InternalClass))] + [JsonSerializable(typeof(StrongTypedProgressType))] + [JsonSerializable(typeof(ProgressWithCompletion))] + [JsonSerializable(typeof(CustomSerializedType))] + [JsonSerializable(typeof(IProgress))] + [JsonSerializable(typeof(IProgress))] + private partial class SourceGenerationContext : JsonSerializerContext; + + [GenerateShapeFor] + [GenerateShapeFor] + [GenerateShapeFor] + [GenerateShapeFor] + [GenerateShapeFor] + [GenerateShapeFor] + [GenerateShapeFor] + [GenerateShapeFor] + [GenerateShapeFor] + private partial class Witness; +} diff --git a/test/StreamJsonRpc.Tests/JsonRpcTests.cs b/test/StreamJsonRpc.Tests/JsonRpcTests.cs index 44f11ab1..39afac63 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -7,9 +7,9 @@ using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; +using System.Text.Json.Serialization; using Microsoft.VisualStudio.Threading; using Nerdbank.Streams; -using PolyType; using JsonNET = Newtonsoft.Json; using STJ = System.Text.Json.Serialization; @@ -343,6 +343,8 @@ public async Task InvokeWithCancellationAsync_ServerMethodSelfCancelsDoesNotRepo [Fact] public async Task CanInvokeMethodThatReturnsTaskOfInternalClass() { + Assert.SkipWhen(this is JsonRpcPolyTypeJsonHeadersTests, "Not (yet) supported."); + // JsonRpc does not invoke non-public members in the default configuration. A public member cannot have Task result. // Though it can have result of just Task type, which carries a NonPublicType instance. InternalClass result = await this.clientRpc.InvokeAsync(nameof(Server.MethodThatReturnsTaskOfInternalClass)); @@ -437,6 +439,8 @@ public async Task CanCallAsyncMethodThatThrowsNonSerializableException(Exception [Theory, PairwiseData] public async Task CanCallAsyncMethodThatThrowsExceptionWithoutDeserializingConstructor(ExceptionProcessing exceptionStrategy) { + Assert.SkipWhen(this is JsonRpcPolyTypeJsonHeadersTests, "Not (yet) supported."); + this.clientRpc.AllowModificationWhileListening = true; this.serverRpc.AllowModificationWhileListening = true; this.clientRpc.ExceptionStrategy = exceptionStrategy; @@ -477,6 +481,8 @@ public async Task CanCallAsyncMethodThatThrowsExceptionWhileSerializingException [Fact] public async Task ThrowCustomExceptionThatImplementsISerializableProperly() { + Assert.SkipWhen(this is JsonRpcPolyTypeJsonHeadersTests, "Not (yet) supported."); + this.clientRpc.AllowModificationWhileListening = true; this.serverRpc.AllowModificationWhileListening = true; this.clientRpc.ExceptionStrategy = ExceptionProcessing.ISerializable; @@ -2420,6 +2426,8 @@ public async Task DisposeOnDisconnect_VsThreadingAsyncDisposable(bool throwFromD [Fact] public async Task SerializableExceptions() { + Assert.SkipWhen(this is JsonRpcPolyTypeJsonHeadersTests, "Not (yet) supported."); + this.serverRpc.AllowModificationWhileListening = true; this.serverRpc.LoadableTypes.Add(typeof(FileNotFoundException)); @@ -3098,6 +3106,8 @@ public async Task InvokeWithParameterObject_WithRenamingAttributes() [Fact] public virtual async Task CanPassAndCallPrivateMethodsObjects() { + Assert.SkipWhen(this is JsonRpcPolyTypeJsonHeadersTests, "Not (yet) supported."); + var result = await this.clientRpc.InvokeAsync(nameof(Server.MethodThatAcceptsFoo), new Foo { Bar = "bar", Bazz = 1000 }); Assert.NotNull(result); Assert.Equal("bar!", result.Bar); @@ -4072,6 +4082,7 @@ public string Property } } + [JsonConverter(typeof(JsonRpcPolyTypeJsonHeadersTests.TypeThrowsWhenDeserializedConverter))] public class TypeThrowsWhenDeserialized { } @@ -4151,6 +4162,46 @@ protected class MockActivityTracingStrategy : IActivityTracingStrategy public void ApplyOutboundActivity(JsonRpcRequest request) => this.Outbound?.Invoke(request); } + /// + /// An exception that throws while being serialized. + /// + [Serializable] + protected class ExceptionMissingDeserializingConstructor : InvalidOperationException + { + public ExceptionMissingDeserializingConstructor(string message) + : base(message) + { + } + } + + [Serializable] + protected class PrivateSerializableException : Exception + { + public PrivateSerializableException() + { + } + + public PrivateSerializableException(string message) + : base(message) + { + } + + public PrivateSerializableException(string message, Exception inner) + : base(message, inner) + { + } + +#if NET8_0_OR_GREATER + [Obsolete] +#endif + protected PrivateSerializableException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { + } + } + /// /// This emulates what .NET Core 2.1 does where async methods actually return an instance of a private derived type. /// @@ -4297,46 +4348,6 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } - /// - /// An exception that throws while being serialized. - /// - [Serializable] - private class ExceptionMissingDeserializingConstructor : InvalidOperationException - { - public ExceptionMissingDeserializingConstructor(string message) - : base(message) - { - } - } - - [Serializable] - private class PrivateSerializableException : Exception - { - public PrivateSerializableException() - { - } - - public PrivateSerializableException(string message) - : base(message) - { - } - - public PrivateSerializableException(string message, Exception inner) - : base(message, inner) - { - } - -#if NET8_0_OR_GREATER - [Obsolete] -#endif - protected PrivateSerializableException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } - } - private class JsonRpcThatSubstitutesType : JsonRpc { public JsonRpcThatSubstitutesType(IJsonRpcMessageHandler messageHandler) diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyPolyTypeJsonTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyPolyTypeJsonTests.cs new file mode 100644 index 00000000..97c1d99c --- /dev/null +++ b/test/StreamJsonRpc.Tests/MarshalableProxyPolyTypeJsonTests.cs @@ -0,0 +1,64 @@ +// 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 PolyTypeJson + +using System.Text.Json; +using System.Text.Json.Serialization; +using StreamJsonRpc.Reflection; +using static JsonRpcTests; + +public partial class MarshalableProxyPolyTypeJsonTests : MarshalableProxyTests +{ + public MarshalableProxyPolyTypeJsonTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override Type FormatterExceptionType => typeof(JsonException); + + protected override IJsonRpcMessageFormatter CreateFormatter() + { + var formatter = new PolyTypeJsonFormatter + { + JsonSerializerOptions = + { + TypeInfoResolver = SourceGenerationContext5.Default, + }, + TypeShapeProvider = PolyType.SourceGenerator.TypeShapeProvider_StreamJsonRpc_Tests.Default, + }; + + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + formatter.RegisterGenericType>(); + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + formatter.RegisterGenericType(); + + return formatter; + } + + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(MessageFormatterEnumerableTracker.EnumeratorResults))] + [JsonSerializable(typeof(ExceptionWithAsyncEnumerable))] + [JsonSerializable(typeof(JsonException))] + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(TypeThrowsWhenSerialized))] + [JsonSerializable(typeof(IAsyncEnumerable))] + [JsonSerializable(typeof(IMarshalable))] + [JsonSerializable(typeof(IMarshalableWithCallScopedLifetime))] + [JsonSerializable(typeof(IMarshalableWithOptionalInterfaces))] + [JsonSerializable(typeof(IMarshalableWithOptionalInterfaces2))] + [JsonSerializable(typeof(IMarshalableSubType2))] + [JsonSerializable(typeof(IMarshalableAndSerializable))] + [JsonSerializable(typeof(DataContainer))] + [JsonSerializable(typeof(ProxyContainer))] + [JsonSerializable(typeof(ProxyContainer))] + [JsonSerializable(typeof(ProxyContainer>))] + private partial class SourceGenerationContext5 : JsonSerializerContext; + + [GenerateShapeFor] + private partial class Witness; +} diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs index 38783a52..feb7e09f 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs @@ -1051,7 +1051,7 @@ public async Task RpcMarshalable_CallScopedLifetime_ObjectReturned() private void AssertIsNot(object obj, Type type) { Assert.False(((IJsonRpcClientProxy)obj).Is(type), $"Object of type {obj.GetType().FullName} is not expected to be assignable to {type.FullName}"); - if (this is not MarshalableProxyNerdbankMessagePackTests) + if (this is not (MarshalableProxyNerdbankMessagePackTests or MarshalableProxyPolyTypeJsonTests)) { Assert.False(type.IsAssignableFrom(obj.GetType()), $"Object of type {obj.GetType().FullName} is not expected to be assignable to {type.FullName}"); } diff --git a/test/StreamJsonRpc.Tests/PolyTypeJsonFormatterTests.cs b/test/StreamJsonRpc.Tests/PolyTypeJsonFormatterTests.cs new file mode 100644 index 00000000..59acabbd --- /dev/null +++ b/test/StreamJsonRpc.Tests/PolyTypeJsonFormatterTests.cs @@ -0,0 +1,80 @@ +// 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 PolyTypeJson + +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Nerdbank.Streams; + +public partial class PolyTypeJsonFormatterTests : FormatterTestBase +{ + public PolyTypeJsonFormatterTests(ITestOutputHelper logger) + : base(logger) + { + } + + [Fact] + public void STJAttributesWinOverDataContractAttributesByDefault() + { + IJsonRpcMessageFactory messageFactory = this.Formatter; + JsonRpcRequest requestMessage = messageFactory.CreateRequestMessage(); + requestMessage.Method = "test"; + requestMessage.Arguments = new[] { new DCSClass { C = 1 } }; + + using Sequence sequence = new(); + this.Formatter.Serialize(sequence, requestMessage); + + using JsonDocument doc = JsonDocument.Parse(sequence); + this.Logger.WriteLine(doc.RootElement.ToString()); + Assert.Equal(1, doc.RootElement.GetProperty("params")[0].GetProperty("B").GetInt32()); + } + + [Fact] + public void STJAttributesWinOverDataMemberWithoutDataContract() + { + IJsonRpcMessageFactory messageFactory = this.Formatter; + JsonRpcRequest requestMessage = messageFactory.CreateRequestMessage(); + requestMessage.Method = "test"; + requestMessage.Arguments = new[] { new STJClass { C = 1 } }; + + using Sequence sequence = new(); + this.Formatter.Serialize(sequence, requestMessage); + + using JsonDocument doc = JsonDocument.Parse(sequence); + this.Logger.WriteLine(doc.RootElement.ToString()); + Assert.Equal(1, doc.RootElement.GetProperty("params")[0].GetProperty("B").GetInt32()); + } + + protected override PolyTypeJsonFormatter CreateFormatter() => new() + { + JsonSerializerOptions = { TypeInfoResolver = SourceGenerationContext2.Default }, + TypeShapeProvider = PolyType.SourceGenerator.TypeShapeProvider_StreamJsonRpc_Tests.Default, + }; + + [DataContract] + public class DCSClass + { + [DataMember(Name = "A")] + [JsonPropertyName("B")] + public int C { get; set; } + } + + public class STJClass + { + [DataMember(Name = "A")] + [JsonPropertyName("B")] + public int C { get; set; } + } + + [JsonSerializable(typeof(DCSClass))] + [JsonSerializable(typeof(STJClass))] + [JsonSerializable(typeof(CustomType))] + [JsonSerializable(typeof(string))] + private partial class SourceGenerationContext2 : JsonSerializerContext; + + [GenerateShapeFor] + [GenerateShapeFor] + private partial class Witness; +} diff --git a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj index ca140cca..53cda5e5 100644 --- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj +++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj @@ -19,14 +19,17 @@ + + + @@ -40,6 +43,7 @@ + @@ -56,6 +60,7 @@ + diff --git a/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs index f4ccb8eb..bfbdf883 100644 --- a/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs +++ b/test/StreamJsonRpc.Tests/SystemTextJsonFormatterTests.cs @@ -13,29 +13,6 @@ public SystemTextJsonFormatterTests(ITestOutputHelper logger) { } - ////[Fact] - ////public void DataContractAttributesWinOverSTJAttributes() - ////{ - //// this.Formatter = new SystemTextJsonFormatter - //// { - //// JsonSerializerOptions = - //// { - //// TypeInfoResolver = new SystemTextJsonFormatter.DataContractResolver(), - //// }, - //// }; - //// IJsonRpcMessageFactory messageFactory = this.Formatter; - //// JsonRpcRequest requestMessage = messageFactory.CreateRequestMessage(); - //// requestMessage.Method = "test"; - //// requestMessage.Arguments = new[] { new DCSClass { C = 1 } }; - - //// using Sequence sequence = new(); - //// this.Formatter.Serialize(sequence, requestMessage); - - //// using JsonDocument doc = JsonDocument.Parse(sequence); - //// this.Logger.WriteLine(doc.RootElement.ToString()); - //// Assert.Equal(1, doc.RootElement.GetProperty("params")[0].GetProperty("A").GetInt32()); - ////} - [Fact] public void STJAttributesWinOverDataContractAttributesByDefault() {