Fix STJ source gen support for System.BinaryData. (#89454)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 26 Jul 2023 15:54:13 +0000 (16:54 +0100)
committerGitHub <noreply@github.com>
Wed, 26 Jul 2023 15:54:13 +0000 (16:54 +0100)
* Fix STJ source gen support for System.BinaryData.

* remove redundant sealed keyword from converter overrides

* Generate compatibility suppressions

* Add comment to compatibility suppressions

src/libraries/System.Memory.Data/ref/System.Memory.Data.cs
src/libraries/System.Memory.Data/src/CompatibilitySuppressions.xml [new file with mode: 0644]
src/libraries/System.Memory.Data/src/System/BinaryData.cs
src/libraries/System.Memory.Data/src/System/BinaryDataConverter.cs
src/libraries/System.Memory.Data/tests/BinaryDataTests.cs
src/libraries/System.Memory.Data/tests/System.Memory.Data.Tests.csproj

index 5dfa892..d142c99 100644 (file)
@@ -6,7 +6,7 @@
 
 namespace System
 {
-    [System.Text.Json.Serialization.JsonConverter(typeof(BinaryDataConverter))]
+    [System.Text.Json.Serialization.JsonConverterAttribute(typeof(System.Text.Json.Serialization.BinaryDataJsonConverter))]
     public partial class BinaryData
     {
         public BinaryData(byte[] data) { }
@@ -17,6 +17,8 @@ namespace System
         public BinaryData(System.ReadOnlyMemory<byte> data) { }
         public BinaryData(string data) { }
         public static System.BinaryData Empty { get { throw null; } }
+        public bool IsEmpty { get { throw null; } }
+        public int Length { get { throw null; } }
         [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
         public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; }
         public static System.BinaryData FromBytes(byte[] data) { throw null; }
@@ -30,8 +32,6 @@ namespace System
         public static System.BinaryData FromString(string data) { throw null; }
         [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
         public override int GetHashCode() { throw null; }
-        public bool IsEmpty { get { throw null; } }
-        public int Length { get { throw null; } }
         public static implicit operator System.ReadOnlyMemory<byte> (System.BinaryData? data) { throw null; }
         public static implicit operator System.ReadOnlySpan<byte> (System.BinaryData? data) { throw null; }
         public byte[] ToArray() { throw null; }
@@ -43,10 +43,13 @@ namespace System
         public System.IO.Stream ToStream() { throw null; }
         public override string ToString() { throw null; }
     }
-
-    internal sealed class BinaryDataConverter : System.Text.Json.Serialization.JsonConverter<BinaryData>
+}
+namespace System.Text.Json.Serialization
+{
+    public sealed partial class BinaryDataJsonConverter : System.Text.Json.Serialization.JsonConverter<System.BinaryData>
     {
-        public sealed override BinaryData? Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
-        public sealed override void Write(System.Text.Json.Utf8JsonWriter writer, BinaryData value, System.Text.Json.JsonSerializerOptions options) { }
+        public BinaryDataJsonConverter() { }
+        public override System.BinaryData? Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
+        public override void Write(System.Text.Json.Utf8JsonWriter writer, System.BinaryData value, System.Text.Json.JsonSerializerOptions options) { }
     }
 }
diff --git a/src/libraries/System.Memory.Data/src/CompatibilitySuppressions.xml b/src/libraries/System.Memory.Data/src/CompatibilitySuppressions.xml
new file mode 100644 (file)
index 0000000..8cf28d2
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
+<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <!-- Move JsonConverterAttribute argument from internal to a public converter type in a new namespace -->
+  <Suppression>
+    <DiagnosticId>CP0015</DiagnosticId>
+    <Target>T:System.BinaryData:[T:System.Text.Json.Serialization.JsonConverterAttribute]</Target>
+    <Left>lib/net462/System.Memory.Data.dll</Left>
+    <Right>lib/net462/System.Memory.Data.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0015</DiagnosticId>
+    <Target>T:System.BinaryData:[T:System.Text.Json.Serialization.JsonConverterAttribute]</Target>
+    <Left>lib/net6.0/System.Memory.Data.dll</Left>
+    <Right>lib/net6.0/System.Memory.Data.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0015</DiagnosticId>
+    <Target>T:System.BinaryData:[T:System.Text.Json.Serialization.JsonConverterAttribute]</Target>
+    <Left>lib/net7.0/System.Memory.Data.dll</Left>
+    <Right>lib/net7.0/System.Memory.Data.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0015</DiagnosticId>
+    <Target>T:System.BinaryData:[T:System.Text.Json.Serialization.JsonConverterAttribute]</Target>
+    <Left>lib/netstandard2.0/System.Memory.Data.dll</Left>
+    <Right>lib/netstandard2.0/System.Memory.Data.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+</Suppressions>
\ No newline at end of file
index 0832a5b..2cd938b 100644 (file)
@@ -16,7 +16,7 @@ namespace System
     /// <summary>
     /// A lightweight abstraction for a payload of bytes that supports converting between string, stream, JSON, and bytes.
     /// </summary>
-    [JsonConverter(typeof(BinaryDataConverter))]
+    [JsonConverter(typeof(BinaryDataJsonConverter))]
     public class BinaryData
     {
         private const string JsonSerializerRequiresDynamicCode = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation.";
@@ -291,15 +291,7 @@ namespace System
         [RequiresDynamicCode(JsonSerializerRequiresDynamicCode)]
         [RequiresUnreferencedCode(JsonSerializerRequiresUnreferencedCode)]
         public T? ToObjectFromJson<T>(JsonSerializerOptions? options = default)
-        {
-            ReadOnlySpan<byte> span = _bytes.Span;
-
-            // Check for the UTF-8 byte order mark (BOM) EF BB BF
-            if (span.Length > 2 && span[0] == 0xEF && span[1] == 0xBB && span[2] == 0xBF)
-                span = span.Slice(3);
-
-            return JsonSerializer.Deserialize<T>(span, options);
-        }
+            => JsonSerializer.Deserialize<T>(GetBytesWithTrimmedBom(), options);
 
         /// <summary>
         /// Converts the <see cref="BinaryData"/> to the specified type using
@@ -310,7 +302,18 @@ namespace System
         /// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> to use when serializing to JSON.</param>
         /// <returns>The data converted to the specified type.</returns>
         public T? ToObjectFromJson<T>(JsonTypeInfo<T> jsonTypeInfo)
-            => JsonSerializer.Deserialize<T>(_bytes.Span, jsonTypeInfo);
+            => JsonSerializer.Deserialize(GetBytesWithTrimmedBom(), jsonTypeInfo);
+
+        private ReadOnlySpan<byte> GetBytesWithTrimmedBom()
+        {
+            ReadOnlySpan<byte> span = _bytes.Span;
+
+            // Check for the UTF-8 byte order mark (BOM) EF BB BF
+            if (span.Length > 2 && span[0] == 0xEF && span[1] == 0xBB && span[2] == 0xBF)
+                span = span.Slice(3);
+
+            return span;
+        }
 
         /// <summary>
         /// Defines an implicit conversion from a <see cref="BinaryData" /> to a <see cref="ReadOnlyMemory{Byte}"/>.
index 2e16ae1..aa144e0 100644 (file)
@@ -1,19 +1,21 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace System
+namespace System.Text.Json.Serialization
 {
-    internal sealed class BinaryDataConverter : JsonConverter<BinaryData>
+    /// <summary>
+    /// Serializes <see cref="BinaryData"/> instances as Base64 JSON strings.
+    /// </summary>
+    public sealed class BinaryDataJsonConverter : JsonConverter<BinaryData>
     {
-        public sealed override BinaryData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        /// <inheritdoc/>
+        public override BinaryData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
             return BinaryData.FromBytes(reader.GetBytesFromBase64());
         }
 
-        public sealed override void Write(Utf8JsonWriter writer, BinaryData value, JsonSerializerOptions options)
+        /// <inheritdoc/>
+        public override void Write(Utf8JsonWriter writer, BinaryData value, JsonSerializerOptions options)
         {
             writer.WriteBase64StringValue(value.ToMemory().Span);
         }
index d3f1fe7..ddf00ce 100644 (file)
@@ -1,7 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Runtime.InteropServices;
@@ -362,7 +361,7 @@ namespace System.Tests
             data = BinaryData.FromObjectAsJson<TestModel>(null);
             Assert.Null(data.ToObjectFromJson<TestModel>());
 
-            data = BinaryData.FromObjectAsJson<TestModel>(null, TestModelJsonContext.Default.TestModel as JsonTypeInfo<TestModel>);
+            data = BinaryData.FromObjectAsJson<TestModel>(null, TestModelJsonContext.Default.TestModel);
             Assert.Null(data.ToObjectFromJson<TestModel>(TestModelJsonContext.Default.TestModel));
         }
 
@@ -374,7 +373,6 @@ namespace System.Tests
 
             ex = await Assert.ThrowsAsync<ArgumentNullException>(() => BinaryData.FromStreamAsync(null));
             Assert.Contains("stream", ex.Message);
-
         }
 
         [Fact]
@@ -413,6 +411,12 @@ namespace System.Tests
             Assert.Equal(payload.A, model.A);
             Assert.Equal(payload.B, model.B);
             Assert.Equal(payload.C, model.C);
+
+            var typeInfo = (JsonTypeInfo<TestModel>)JsonSerializerOptions.Default.GetTypeInfo(typeof(TestModel));
+            model = data.ToObjectFromJson<TestModel>(typeInfo);
+            Assert.Equal(payload.A, model.A);
+            Assert.Equal(payload.B, model.B);
+            Assert.Equal(payload.C, model.C);
         }
 
         [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBuiltWithAggressiveTrimming))]
@@ -701,7 +705,7 @@ namespace System.Tests
             Assert.False(data.IsEmpty);
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBuiltWithAggressiveTrimming))]
         public void IsBinaryDataMemberPropertySerialized()
         {
             var data = new BinaryData("A test value");
@@ -714,7 +718,7 @@ namespace System.Tests
             Assert.Equal(jsonTestModel, serializedTestModel);           
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBuiltWithAggressiveTrimming))]
         public void IsBinaryDataMemberPropertyDeserialized()
         {
             var data = new BinaryData("A test value");
@@ -726,6 +730,31 @@ namespace System.Tests
             Assert.Equal(data.ToString(), deserializedModel.A.ToString());
         }
 
+        [Fact]
+        public void IsBinaryDataMemberPropertySerialized_SourceGen()
+        {
+            var data = new BinaryData("A test value");
+            var dataBase64 = Convert.ToBase64String(data.ToArray());
+            var jsonTestModel = $"{{\"A\":\"{dataBase64}\"}}";
+            TestModelWithBinaryDataProperty testModel = new TestModelWithBinaryDataProperty { A = data };
+
+            var serializedTestModel = JsonSerializer.Serialize(testModel, TestModelWithBinaryDataPropertyContext.Default.TestModelWithBinaryDataProperty);
+
+            Assert.Equal(jsonTestModel, serializedTestModel);
+        }
+
+        [Fact]
+        public void IsBinaryDataMemberPropertyDeserialized_SourceGen()
+        {
+            var data = new BinaryData("A test value");
+            var dataBase64 = Convert.ToBase64String(data.ToArray());
+            var jsonTestModel = $"{{\"A\":\"{dataBase64}\"}}";
+
+            TestModelWithBinaryDataProperty deserializedModel = JsonSerializer.Deserialize<TestModelWithBinaryDataProperty>(jsonTestModel, TestModelWithBinaryDataPropertyContext.Default.TestModelWithBinaryDataProperty);
+
+            Assert.Equal(data.ToString(), deserializedModel.A.ToString());
+        }
+
         internal class TestModel
         {
             public string A { get; set; }
@@ -778,5 +807,9 @@ namespace System.Tests
         {
             public BinaryData A { get; set; }
         }
+
+        [JsonSerializable(typeof(TestModelWithBinaryDataProperty))]
+        internal partial class TestModelWithBinaryDataPropertyContext : JsonSerializerContext
+        { }
     }
 }
index 6293ca3..b491d25 100644 (file)
@@ -2,7 +2,6 @@
 
   <PropertyGroup>
     <TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkMinimum)</TargetFrameworks>
-    <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
   </PropertyGroup>
 
   <ItemGroup>