From a8f594e5c06af26bc0a051c7c8783b1c9c24fdab Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 2 Jul 2021 23:33:10 +0100 Subject: [PATCH 01/12] Add json support for F# options, lists, sets, maps and records --- .../System.Text.Json/System.Text.Json.sln | 61 ++--- .../src/Resources/Strings.resx | 5 +- .../src/System.Text.Json.csproj | 6 + .../Converters/FSharp/FSharpListConverter.cs | 73 ++++++ .../Converters/FSharp/FSharpMapConverter.cs | 89 ++++++++ .../FSharp/FSharpOptionConverter.cs | 56 +++++ .../Converters/FSharp/FSharpSetConverter.cs | 73 ++++++ .../FSharp/FSharpTypeConverterFactory.cs | 60 +++++ .../Object/ObjectConverterFactory.cs | 10 +- .../Text/Json/Serialization/JsonConverter.cs | 5 + .../Json/Serialization/JsonConverterOfT.cs | 11 +- .../JsonSerializerOptions.Converters.cs | 1 + .../Metadata/FSharpCoreReflectionProxy.cs | 209 ++++++++++++++++++ .../Text/Json/ThrowHelper.Serialization.cs | 7 + .../CollectionTests.fs | 177 +++++++++++++++ .../System.Text.Json.FSharp.Tests/Helpers.fs | 19 ++ .../OptionTests.fs | 154 +++++++++++++ .../RecordTests.fs | 51 +++++ .../System.Text.Json.FSharp.Tests.fsproj | 14 ++ 19 files changed, 1051 insertions(+), 30 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/Helpers.fs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index 433c378f98852a..a27abbe6bc9774 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31430.445 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" @@ -35,35 +39,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{676B6044-FA4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{74017ACD-3AC1-4BB5-804B-D57E305FFBD9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" EndProject Global - GlobalSection(NestedProjects) = preSolution - {102945CA-3736-4B2C-8E68-242A0B247F2B} = {3C544454-BD8B-44F4-A174-B61F18957613} - {73D5739C-E382-4E22-A7D3-B82705C58C74} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} - {25C42754-B384-4842-8FA7-75D7A79ADF0D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} - {4774F56D-16A8-4ABB-8C73-5F57609F1773} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} - {E2077991-EB83-471C-B17F-72F569FFCE6D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} - {BE230195-2A1C-4674-BACB-502C2CD864E9} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} - {D7276D7D-F117-47C5-B514-8E3E964769BE} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} - {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} - {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} - {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} - {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} - {7909EB27-0D6E-46E6-B9F9-8A1EFD557018} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} - {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} - {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} - {6485EED4-C313-4551-9865-8ADCED603629} = {74017ACD-3AC1-4BB5-804B-D57E305FFBD9} - {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {3C544454-BD8B-44F4-A174-B61F18957613} - {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {3C544454-BD8B-44F4-A174-B61F18957613} - {18173CEC-895F-4F62-B7BB-B724457FEDCD} = {3C544454-BD8B-44F4-A174-B61F18957613} - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -141,10 +127,35 @@ Global {18173CEC-895F-4F62-B7BB-B724457FEDCD}.Debug|Any CPU.Build.0 = Debug|Any CPU {18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.ActiveCfg = Release|Any CPU {18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.Build.0 = Release|Any CPU + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {102945CA-3736-4B2C-8E68-242A0B247F2B} = {3C544454-BD8B-44F4-A174-B61F18957613} + {73D5739C-E382-4E22-A7D3-B82705C58C74} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} + {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} + {25C42754-B384-4842-8FA7-75D7A79ADF0D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} + {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} + {4774F56D-16A8-4ABB-8C73-5F57609F1773} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} + {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} + {E2077991-EB83-471C-B17F-72F569FFCE6D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} + {7909EB27-0D6E-46E6-B9F9-8A1EFD557018} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} + {BE230195-2A1C-4674-BACB-502C2CD864E9} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} + {D7276D7D-F117-47C5-B514-8E3E964769BE} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} + {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} + {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} + {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} + {6485EED4-C313-4551-9865-8ADCED603629} = {74017ACD-3AC1-4BB5-804B-D57E305FFBD9} + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {3C544454-BD8B-44F4-A174-B61F18957613} + {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {3C544454-BD8B-44F4-A174-B61F18957613} + {18173CEC-895F-4F62-B7BB-B724457FEDCD} = {3C544454-BD8B-44F4-A174-B61F18957613} + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {3C544454-BD8B-44F4-A174-B61F18957613} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5868B757-D821-41FC-952E-2113A0519506} EndGlobalSection diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index e654d368051dc2..5e059b3886813d 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -611,4 +611,7 @@ A 'field' member cannot be 'virtual'. See arguments for the '{0}' and '{1}' parameters. - \ No newline at end of file + + Could not locate required member '{0}' from FSharp.Core. + + diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 08855731abe63e..848d0ad891ffbd 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -97,6 +97,7 @@ + @@ -132,6 +133,11 @@ + + + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs new file mode 100644 index 00000000000000..4c4116bf02ca1b --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + // Converter for F# lists: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-list-1.html + internal sealed class FSharpListConverter : IEnumerableDefaultConverter + where TList : IEnumerable + { + private readonly Func, TList> _listConstructor; + + public FSharpListConverter() + { + _listConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpListConstructor(); + } + + protected override void Add(in TElement value, ref ReadStack state) + { + ((List)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = new List(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = _listConstructor((List)state.Current.ReturnValue!); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TList value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + return true; + } + } + else + { + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + enumerator.Dispose(); + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs new file mode 100644 index 00000000000000..12d6d1bfd78c6e --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + // Converter for F# maps: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-fsharpmap-2.html + internal sealed class FSharpMapConverter : DictionaryDefaultConverter + where TMap : IEnumerable> + where TKey : notnull + { + private readonly Func>, TMap> _mapConstructor; + + public FSharpMapConverter() + { + _mapConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpMapConstructor(); + } + + protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state) + { + ((List>)state.Current.ReturnValue!).Add (new Tuple(key, value)); + } + + internal override bool CanHaveIdMetadata => false; + + protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state) + { + state.Current.ReturnValue = new List>(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = _mapConstructor((List>)state.Current.ReturnValue!); + } + + protected internal override bool OnWriteResume(Utf8JsonWriter writer, TMap value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator> enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + return true; + } + } + else + { + enumerator = (IEnumerator>)state.Current.CollectionEnumerator; + } + + JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; + _keyConverter ??= GetConverter(typeInfo.KeyTypeInfo!); + _valueConverter ??= GetConverter(typeInfo.ElementTypeInfo!); + + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + if (state.Current.PropertyState < StackFramePropertyState.Name) + { + state.Current.PropertyState = StackFramePropertyState.Name; + + TKey key = enumerator.Current.Key; + _keyConverter.WriteWithQuotes(writer, key, options, ref state); + } + + TValue element = enumerator.Current.Value; + if (!_valueConverter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + state.Current.EndDictionaryElement(); + } while (enumerator.MoveNext()); + + enumerator.Dispose(); + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs new file mode 100644 index 00000000000000..a11b5da88753e8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + // Converter for F# optional values: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-option-1.html + // Serializes `Some(value)` using the format of `value` and `None` values as `null`. + internal sealed class FSharpOptionConverter : JsonResumableConverter + where TOption : class + { + // While technically not implementing IEnumerable, F# optionals are effectively generic collections of at most one element. + internal override ConverterStrategy ConverterStrategy => ConverterStrategy.Enumerable; + internal override Type? ElementType => typeof(TElement); + + private readonly Func _optionValueGetter; + private readonly Func _optionConstructor; + + public FSharpOptionConverter(JsonConverter elementConverter) + { + _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionValueGetter(); + _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionConstructor(); + // If the element converter is value, this converter will also be writing values + // Set a flag to signal this fact to the covnerter infrastracture. + CanWriteJsonValues = elementConverter.ConverterStrategy == ConverterStrategy.Value; + } + + internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TOption? value) + { + state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + var elementConverter = (JsonConverter)state.Current.JsonPropertyInfo.ConverterBase; + + if (elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element)) + { + // null element values are deserialized as 'None'. + value = element is null ? null : _optionConstructor(element); + return true; + } + + value = null; + return false; + } + + internal override bool OnTryWrite(Utf8JsonWriter writer, TOption value, JsonSerializerOptions options, ref WriteStack state) + { + Debug.Assert(value is not null); // 'None' values are encoded as null: handled by the base converter. + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + var elementConverter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo.ConverterBase; + + TElement element = _optionValueGetter(value); + return elementConverter.TryWrite(writer, element, options, ref state); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs new file mode 100644 index 00000000000000..0597e80087f6b6 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + // Converter for F# sets: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-fsharpset-1.html + internal sealed class FSharpSetConverter : IEnumerableDefaultConverter + where TSet : IEnumerable + { + private readonly Func, TSet> _setConstructor; + + public FSharpSetConverter() + { + _setConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpSetConstructor(); + } + + protected override void Add(in TElement value, ref ReadStack state) + { + ((List)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = new List(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = _setConstructor((List)state.Current.ReturnValue!); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TSet value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + return true; + } + } + else + { + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + enumerator.Dispose(); + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs new file mode 100644 index 00000000000000..3cb3d315bd526f --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + internal class FSharpTypeConverterFactory : JsonConverterFactory + { + private ObjectConverterFactory? _recordConverterFactory; + + public override bool CanConvert(Type typeToConvert) => + FSharpCoreReflectionProxy.IsFSharpType(typeToConvert) && + FSharpCoreReflectionProxy.Instance.DetectFSharpKind(typeToConvert) is not FSharpKind.Unrecognized; + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(CanConvert(typeToConvert)); + + Type elementType; + Type converterFactoryType; + object?[]? constructorArguments = null; + + switch (FSharpCoreReflectionProxy.Instance.DetectFSharpKind(typeToConvert)) + { + case FSharpKind.Option: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpOptionConverter<,>).MakeGenericType(typeToConvert, elementType); + constructorArguments = new object[] { options.GetConverterInternal(elementType) }; + break; + case FSharpKind.List: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpListConverter<,>).MakeGenericType(typeToConvert, elementType); + break; + case FSharpKind.Set: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpSetConverter<,>).MakeGenericType(typeToConvert, elementType); + break; + case FSharpKind.Map: + Type[] genericArgs = typeToConvert.GetGenericArguments(); + Type keyType = genericArgs[0]; + Type valueType = genericArgs[1]; + converterFactoryType = typeof(FSharpMapConverter<,,>).MakeGenericType(typeToConvert, keyType, valueType); + break; + case FSharpKind.Record: + // Use a modified object converter factory that picks the right constructor for struct record deserialization. + ObjectConverterFactory objectFactory = _recordConverterFactory ??= new ObjectConverterFactory(useDefaultConstructorInUnannotatedStructs: false); + Debug.Assert(objectFactory.CanConvert(typeToConvert)); + return objectFactory.CreateConverter(typeToConvert, options); + + default: + Debug.Fail("Unrecognized F# type."); + throw new Exception(); + } + + return (JsonConverter)Activator.CreateInstance(converterFactoryType, constructorArguments)!; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs index 1a3903a5327b0e..aee6ff86db31fc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs @@ -15,8 +15,14 @@ namespace System.Text.Json.Serialization.Converters /// internal sealed class ObjectConverterFactory : JsonConverterFactory { + // Need to toggle this behavior when generating converters for F# struct records. + private readonly bool _useDefaultConstructorInUnannotatedStructs; + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - public ObjectConverterFactory() { } + public ObjectConverterFactory(bool useDefaultConstructorInUnannotatedStructs = true) + { + _useDefaultConstructorInUnannotatedStructs = useDefaultConstructorInUnannotatedStructs; + } public override bool CanConvert(Type typeToConvert) { @@ -164,7 +170,7 @@ private JsonConverter CreateKeyValuePairConverter(Type type, JsonSerializerOptio } // Structs will use default constructor if attribute isn't used. - if (type.IsValueType && ctorWithAttribute == null) + if (_useDefaultConstructorInUnannotatedStructs && type.IsValueType && ctorWithAttribute == null) { return null; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index f324f672c915f7..c51a2723b67c90 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -34,6 +34,11 @@ internal JsonConverter() { } internal bool CanBePolymorphic { get; set; } + /// + /// When set, indicates a non-value converter that is capable of writing or reading simple JSON values. + /// + internal bool CanWriteJsonValues { get; set; } + /// /// Used to support JsonObject as an extension property in a loosely-typed, trimmable manner. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 33f45fd3ceda5f..27ba9a8965f4c9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -249,6 +249,8 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali else #endif { + long originalBytesConsumed = 0; + if (!wasContinuation) { // For perf and converter simplicity, handle null here instead of forwarding to the converter. @@ -272,6 +274,10 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali Debug.Assert(state.Current.OriginalDepth == 0); state.Current.OriginalDepth = reader.CurrentDepth; + + // Do not store in the ReadStack since it will only be used if the converter + // behaves like a value converter (hence no continuations are expected to occur). + originalBytesConsumed = reader.BytesConsumed; } success = OnTryRead(ref reader, typeToConvert, options, ref state, out value); @@ -286,8 +292,8 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali VerifyRead( state.Current.OriginalTokenType, state.Current.OriginalDepth, - bytesConsumed: 0, - isValueConverter: false, + bytesConsumed: originalBytesConsumed, + isValueConverter: CanWriteJsonValues, ref reader); // No need to clear state.Current.* since a stack pop will occur. @@ -545,6 +551,7 @@ internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, break; default: + // A non-value converter (object or collection) should always have Start and End tokens. // A value converter should not make any reads. if (!isValueConverter || reader.BytesConsumed != bytesConsumed) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index a5270028def60d..27bcc4fb961c1f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -38,6 +38,7 @@ private void RootBuiltInConverters() new NullableConverterFactory(), new EnumConverterFactory(), new JsonNodeConverterFactory(), + new FSharpTypeConverterFactory(), // IAsyncEnumerable takes precedence over IEnumerable. new IAsyncEnumerableConverterFactory(), // IEnumerable should always be second to last since they can convert any IEnumerable. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs new file mode 100644 index 00000000000000..0c5ad107ee51de --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace System.Text.Json.Serialization.Metadata +{ + // Recognizing types emitted by the F# compiler requires consuming APIs from the FSharp.Core runtime library. + // Every F# application ships with a copy of FSharp.Core, however it is not available statically to System.Text.Json. + // The following class uses reflection to access the relevant APIs required to detect the various F# types we are looking to support. + + /// + /// The various categories of F# types that System.Text.Json supports. + /// + internal enum FSharpKind + { + Unrecognized, Option, List, Set, Map, Record + } + + /// + /// Proxy class used to access FSharp.Core metadata and reflection APIs that are not statically available to System.Text.Json. + /// + internal sealed class FSharpCoreReflectionProxy + { + private static object s_lockObj = new object(); + private static FSharpCoreReflectionProxy? s_singletonInstance; + + // Every type generated by the F# compiler is annotated with the CompilationMappingAttribute + // containing all relevant metadata required to determine its kind: + // https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-compilationmappingattribute.html#SourceConstructFlags + private const string CompilationMappingAttributeTypeName = "Microsoft.FSharp.Core.CompilationMappingAttribute"; + private readonly Type _compilationMappingAttributeType; + private readonly MethodInfo? _sourceConstructFlagsGetter; + + private readonly Type? _fsharpOptionType; + private readonly Type? _fsharpListType; + private readonly Type? _fsharpSetType; + private readonly Type? _fsharpMapType; + + private readonly MethodInfo? _fsharpListCtor; + private readonly MethodInfo? _fsharpSetCtor; + private readonly MethodInfo? _fsharpMapCtor; + + /// + /// Checks if the provided System.Type instance is emitted by the F# compiler. + /// If true, also initializes the proxy singleton for future by other F# types. + /// + public static bool IsFSharpType(Type type) + { + if (s_singletonInstance is null) + { + if (GetFSharpCoreAssembly(type) is Assembly fsharpCoreAssembly) + { + // Type is F# type, initialize the singleton instance. + lock (s_lockObj) + { + s_singletonInstance ??= new FSharpCoreReflectionProxy(fsharpCoreAssembly); + } + + return true; + } + + return false; + } + + return s_singletonInstance.GetFSharpCompilationMappingAttribute(type) is not null; + } + + /// + /// Gets the singleton proxy instance; prerequires a successful IsFSharpType call for proxy initialization. + /// + public static FSharpCoreReflectionProxy Instance + { + get + { + Debug.Assert(s_singletonInstance is not null, "should be initialized via a successful IsFSharpType call."); + return s_singletonInstance; + } + } + + private FSharpCoreReflectionProxy(Assembly fsharpCoreAssembly) + { + Debug.Assert(fsharpCoreAssembly.GetName().Name == "FSharp.Core"); + + _compilationMappingAttributeType = fsharpCoreAssembly.GetType(CompilationMappingAttributeTypeName)!; + _sourceConstructFlagsGetter = _compilationMappingAttributeType.GetProperty("SourceConstructFlags")?.GetGetMethod(); + + _fsharpOptionType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); + _fsharpListType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpList`1"); + _fsharpSetType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpSet`1"); + _fsharpMapType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpMap`2"); + + _fsharpListCtor = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.ListModule")?.GetMethod("OfSeq", BindingFlags.Public | BindingFlags.Static); + _fsharpSetCtor = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.SetModule")?.GetMethod("OfSeq", BindingFlags.Public | BindingFlags.Static); + _fsharpMapCtor = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.MapModule")?.GetMethod("OfSeq", BindingFlags.Public | BindingFlags.Static); + } + + public FSharpKind DetectFSharpKind(Type type) + { + Attribute? compilationMappingAttribute = GetFSharpCompilationMappingAttribute(type); + + if (compilationMappingAttribute is null) + { + return FSharpKind.Unrecognized; + } + + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + if (genericType == _fsharpOptionType) return FSharpKind.Option; + if (genericType == _fsharpListType) return FSharpKind.List; + if (genericType == _fsharpSetType) return FSharpKind.Set; + if (genericType == _fsharpMapType) return FSharpKind.Map; + } + + return (GetSourceConstructFlags(compilationMappingAttribute) & SourceConstructFlags.KindMask) switch + { + SourceConstructFlags.RecordType => FSharpKind.Record, + _ => FSharpKind.Unrecognized + }; + } + + public Func CreateFSharpOptionValueGetter() + { + Debug.Assert(_fsharpOptionType!.MakeGenericType(typeof(T)) == typeof(TFSharpOption)); + MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpOption).GetProperty("Value", BindingFlags.Public | BindingFlags.Instance)?.GetGetMethod(), "Option.Value"); + return CreateDelegate>(valueGetter); + } + + public Func CreateFSharpOptionConstructor() + { + Debug.Assert(_fsharpOptionType!.MakeGenericType(typeof(T)) == typeof(TFSharpOption)); + MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Option.Some"); + return CreateDelegate>(methodInfo); + } + + public Func, TFSharpList> CreateFSharpListConstructor() + { + Debug.Assert(_fsharpListType!.MakeGenericType(typeof(T)) == typeof(TFSharpList)); + return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "List.ofSeq").MakeGenericMethod(typeof(T))); + } + + public Func, TFSharpSet> CreateFSharpSetConstructor() + { + Debug.Assert(_fsharpSetType!.MakeGenericType(typeof(T)) == typeof(TFSharpSet)); + return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Set.ofSeq").MakeGenericMethod(typeof(T))); + } + + public Func>, TFSharpMap> CreateFSharpMapConstructor() + { + Debug.Assert(_fsharpMapType!.MakeGenericType(typeof(TKey), typeof(TValue)) == typeof(TFSharpMap)); + return CreateDelegate>, TFSharpMap>>(EnsureMemberExists(_fsharpMapCtor, "Map.ofSeq").MakeGenericMethod(typeof(TKey), typeof(TValue))); + } + + private Attribute? GetFSharpCompilationMappingAttribute(Type type) + => type.GetCustomAttribute(_compilationMappingAttributeType, inherit: true); + + private SourceConstructFlags GetSourceConstructFlags(Attribute compilationMappingAttribute) + => _sourceConstructFlagsGetter is null ? SourceConstructFlags.None : (SourceConstructFlags)_sourceConstructFlagsGetter.Invoke(compilationMappingAttribute, null)!; + + // If the provided type is generated by the F# compiler, returns the runtime FSharp.Core assembly. + private static Assembly? GetFSharpCoreAssembly(Type type) + { + foreach (Attribute attr in type.GetCustomAttributes(inherit: true)) + { + Type attributeType = attr.GetType(); + if (attributeType.FullName == CompilationMappingAttributeTypeName) + { + return attributeType.Assembly; + } + } + + return null; + } + + private static TDelegate CreateDelegate(MethodInfo methodInfo) where TDelegate : Delegate + => (TDelegate)Delegate.CreateDelegate(typeof(TDelegate), methodInfo, throwOnBindFailure: true)!; + + private TMemberInfo EnsureMemberExists(TMemberInfo? memberInfo, string memberName) where TMemberInfo : MemberInfo + { + if (memberInfo is null) + { + ThrowHelper.ThrowMissingMemberException_MissingFSharpCoreMember(memberName); + } + + return memberInfo; + } + + // Replicates the F# source construct flags enum + // https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-sourceconstructflags.html + private enum SourceConstructFlags + { + None = 0, + SumType = 1, + RecordType = 2, + ObjectType = 3, + Field = 4, + Exception = 5, + Closure = 6, + Module = 7, + UnionCase = 8, + Value = 9, + KindMask = 31, + NonPublicRepresentation = 32 + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 61d953ac94ce2e..b8df6c1e10d266 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -716,5 +716,12 @@ public static void ThrowInvalidOperationException_NoDefaultOptionsForContext(Jso { throw new InvalidOperationException(SR.Format(SR.NoDefaultOptionsForContext, context.GetType(), type)); } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowMissingMemberException_MissingFSharpCoreMember(string missingFsharpCoreMember) + { + throw new MissingMemberException(SR.Format(SR.MissingFSharpCoreMember, missingFsharpCoreMember)); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs new file mode 100644 index 00000000000000..af4326d9ccb4bb --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs @@ -0,0 +1,177 @@ +module System.Text.Json.Tests.FSharp.CollectionTests + +open System.Text.Json +open System.Text.Json.Tests.FSharp.Helpers +open Xunit + +let getListsAndSerializations() = seq { + let wrapArgs (list : 'T list) (json : string) = [| box list ; box json |] + + wrapArgs [] "[]" + wrapArgs [1] "[1]" + wrapArgs [1;2;1;2;3;2;2;1;3;3;3] "[1,2,1,2,3,2,2,1,3,3,3]" + wrapArgs [false;true] "[false,true]" + wrapArgs [3.14] "[3.14]" + wrapArgs ["apple";"banana";"cherry"] """["apple","banana","cherry"]""" + wrapArgs [{| x = 0 ; y = 1 |}] """[{"x":0,"y":1}]""" + wrapArgs Unchecked.defaultof "null" // we support null list serialization and deserialization +} + +[] +[] +let ``Lists should have expected serialization`` (list : 'T list) (expectedJson : string) = + let actualJson = JsonSerializer.Serialize list + Assert.Equal(expectedJson, actualJson) + +[] +[] +let ``Lists should have expected deserialization``(expectedList : 'T list) (json : string) = + let actualList = JsonSerializer.Deserialize<'T list> json + Assert.Equal<'T>(expectedList, actualList) + +[] +[] +[] +[] +[] +[] +let ``List deserialization should reject invalid inputs``(json : string) = + Assert.Throws(fun () -> JsonSerializer.Deserialize(json) |> ignore) |> ignore + +[] +let ``List async serialization should be supported``() = async { + let inputs = [1 .. 200] + let expectedJson = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(inputs, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``List async deserialization should be supported``() = async { + let inputs = [1 .. 200] + let json = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync(json, options) + + Assert.Equal(inputs, result) +} + +let getSetsAndSerializations() = seq { + let wrapArgs (set : Set<'T>) (json : string) = [| box set ; box json |] + + wrapArgs Set.empty "[]" + wrapArgs (set [1]) "[1]" + wrapArgs (set [1;2;3]) "[1,2,3]" + wrapArgs (set [false;true]) "[false,true]" + wrapArgs (set [3.14]) "[3.14]" + wrapArgs (set ["apple";"banana";"cherry"]) """["apple","banana","cherry"]""" + wrapArgs (set [{| x = 0 ; y = 1 |}]) """[{"x":0,"y":1}]""" + wrapArgs Unchecked.defaultof> "null" // we support null set serialization and deserialization +} + +[] +[] +let ``Sets should have expected serialization`` (set : Set<'T>) (expectedJson : string) = + let actualJson = JsonSerializer.Serialize set + Assert.Equal(expectedJson, actualJson) + +[] +[] +let ``Sets should have expected deserialization`` (expectedSet : Set<'T>) (json : string) = + let actualSet = JsonSerializer.Deserialize> json + Assert.Equal>(expectedSet, actualSet) + +[] +let ``Set deserialization should trim duplicate elements`` () = + let expectedSet = set [1;2;3] + let actualSet = JsonSerializer.Deserialize> "[1,2,1,2,3,2,2,1,3,3,3]" + Assert.Equal>(expectedSet, actualSet) + +[] +[] +[] +[] +[] +[] +let ``Set deserialization should reject invalid inputs``(json : string) = + Assert.Throws(fun () -> JsonSerializer.Deserialize>(json) |> ignore) |> ignore + +[] +let ``Set async serialization should be supported``() = async { + let inputs = set [1 .. 200] + let expectedJson = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(inputs, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Set async deserialization should be supported``() = async { + let inputs = set [1 .. 200] + let json = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync>(json, options) + + Assert.Equal>(inputs, result) +} + +let getMapsAndSerializations() = seq { + let wrapArgs (set : Map<'K,'V>) (json : string) = [| box set ; box json |] + + wrapArgs Map.empty "{}" + wrapArgs (Map.ofList [("key", "value")]) """{"key":"value"}""" + wrapArgs (Map.ofList [(1, 1); (2, 1)]) """{"1":1,"2":1}""" + wrapArgs (Map.ofList [(false, 1); (true, 1)]) """{"False":1,"True":1}""" + wrapArgs (Map.ofList [("fruit", ["apple";"banana";"cherry"])]) """{"fruit":["apple","banana","cherry"]}""" + wrapArgs (Map.ofList [("coordinates", {| x = 0 ; y = 1 |})]) """{"coordinates":{"x":0,"y":1}}""" + wrapArgs Unchecked.defaultof> "null" // we support null set serialization and deserialization +} + +[] +[] +let ``Maps should have expected serialization`` (map : Map<'key, 'value>) (expectedJson : string) = + let actualJson = JsonSerializer.Serialize map + Assert.Equal(expectedJson, actualJson) + +[] +[] +let ``Maps should have expected deserialization`` (expectedMap : Map<'key, 'value>) (json : string) = + let actualMap = JsonSerializer.Deserialize> json + Assert.Equal>(expectedMap, actualMap) + +[] +[] +[] +[] +[] +let ``Map deserialization should reject invalid inputs``(json : string) = + Assert.Throws(fun () -> JsonSerializer.Deserialize>(json) |> ignore) |> ignore + +[] +let ``Map async serialization should be supported``() = async { + let inputs = Map.ofList [for i in 1 .. 200 -> (i.ToString(), i)] + let expectedJson = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(inputs, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Map async deserialization should be supported``() = async { + let inputs = Map.ofList [for i in 1 .. 200 -> (i.ToString(), i)] + let json = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync>(json, options) + + Assert.Equal>(inputs, result) +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/Helpers.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/Helpers.fs new file mode 100644 index 00000000000000..b2310053bebca5 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/Helpers.fs @@ -0,0 +1,19 @@ +module System.Text.Json.Tests.FSharp.Helpers + +open System.IO +open System.Text +open System.Text.Json + +type JsonSerializer with + static member SerializeAsync<'T>(value : 'T, options : JsonSerializerOptions) = async { + let! ct = Async.CancellationToken + use mem = new MemoryStream() + do! JsonSerializer.SerializeAsync(mem, value, options, ct) |> Async.AwaitTask + return Encoding.UTF8.GetString(mem.ToArray()) + } + + static member DeserializeAsync<'T>(json : string, options : JsonSerializerOptions) = async { + let! ct = Async.CancellationToken + use mem = new MemoryStream(Encoding.UTF8.GetBytes json) + return! JsonSerializer.DeserializeAsync<'T>(mem, options).AsTask() |> Async.AwaitTask + } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs new file mode 100644 index 00000000000000..54796a5184883d --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs @@ -0,0 +1,154 @@ +module System.Text.Json.Tests.FSharp.OptionTests + +open System.Text.Json +open System.Text.Json.Tests.FSharp.Helpers +open Xunit + +let getOptionalElementInputs() = seq { + let wrap value = [| box value |] + + wrap 42 + wrap false + wrap "string" + wrap [|1..5|] + wrap (3,2) + wrap {| Name = "Mary" ; Age = 32 |} + wrap struct {| Name = "Mary" ; Age = 32 |} +} + +[] +[] +let ``Root-level None should serialize as null``(_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T option>(None) + Assert.Equal(expected, actual) + +[] +[] +let ``None property should serialize as null``(_ : 'T) = + let expected = """{"value":null}""" + let actual = JsonSerializer.Serialize {| value = Option<'T>.None |} + Assert.Equal(expected, actual) + +[] +[] +let ``None collection element should serialize as null``(_ : 'T) = + let expected = """[null]""" + let actual = JsonSerializer.Serialize [| Option<'T>.None |] + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level Some should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Serialize(Some value) + Assert.Equal(expected, actual) + +[] +[] +let ``Some property should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Serialize {| value = Some value |} + Assert.Equal(expected, actual) + +[] +[] +let ``Some collection element should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize [|value|] + let actual = JsonSerializer.Serialize [|Some value|] + Assert.Equal(expected, actual) + +[] +let ``Some of null sgetOptionalElementInputs`` () = + let expected = "null" + let actual = JsonSerializer.Serialize(Some null) + Assert.Equal(expected, actual) + +[] +[] +let ``Some of None should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T option option>(Some None) + Assert.Equal(expected, actual) + +[] +[] +let ``Some of Some of None should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T option option option>(Some (Some None)) + Assert.Equal(expected, actual) + +[] +[] +let ``Some of Some of value should serialize as value`` (value : 'T) = + let expected = JsonSerializer.Serialize value + let actual = JsonSerializer.Serialize(Some (Some value)) + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level null should deserialize as None``(_ : 'T) = + let actual = JsonSerializer.Deserialize<'T option>("null") + Assert.Equal(None, actual) + +[] +[] +let ``Null property getOptionalElementInputsne``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T option |}>("""{"value":null}""") + Assert.Equal(None, actual.value) + +[] +[] +let ``Missing propergetOptionalElementInputs None``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T option |}>("{}") + Assert.Equal(None, actual.value) + +[] +[] +let ``Null element sgetOptionalElementInputse``(_ : 'T) = + let expected = [Option<'T>.None] + let actual = JsonSerializer.Deserialize<'T option []>("""[null]""") + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level value should deserialize as Some``(value : 'T) = + let json = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Deserialize<'T option>(json) + Assert.Equal(Some value, actual) + +[] +[] +let ``Property value should deserialize as Some``(value : 'T) = + let json = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Deserialize<{| value : 'T option|}>(json) + Assert.Equal(Some value, actual.value) + +[] +[] +let ``Collection element should deserialize as Some``(value : 'T) = + let json = JsonSerializer.Serialize [| value |] + let actual = JsonSerializer.Deserialize<'T option []>(json) + Assert.Equal([Some value], actual) + +[] +let ``Optional value should support resumable serialization``() = async { + let valueToSerialize = {| Values = Some [|1 .. 200|] |} + let expectedJson = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(valueToSerialize, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Optional value should support resumable deserialization``() = async { + let valueToSerialize = {| Values = Some [|1 .. 200|] |} + let json = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync<{| Values : int [] option |}>(json, options) + + Assert.Equal(valueToSerialize, result) +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs new file mode 100644 index 00000000000000..638ec97add2632 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs @@ -0,0 +1,51 @@ +module System.Text.Json.Tests.FSharp.RecordTests + +open System.Text.Json +open System.Text.Json.Tests.FSharp.Helpers +open Xunit +open System.Text.Json.Serialization + +type MyRecord = + { + Name : string + MiddleName : string option + LastName : string + Age : int + IsActive : bool + } +with + static member Value = { Name = "John" ; MiddleName = None ; LastName = "Doe" ; Age = 34 ; IsActive = true } + static member ExpectedJson = """{"Name":"John","MiddleName":null,"LastName":"Doe","Age":34,"IsActive":true}""" + +[] +let ``Support F# record serialization``() = + let actualJson = JsonSerializer.Serialize(MyRecord.Value) + Assert.Equal(MyRecord.ExpectedJson, actualJson) + +[] +let ``Support F# record deserialization``() = + let result = JsonSerializer.Deserialize(MyRecord.ExpectedJson) + Assert.Equal(MyRecord.Value, result) + +[] +type MyStructRecord = + { + Name : string + MiddleName : string option + LastName : string + Age : int + IsActive : bool + } +with + static member Value = { Name = "John" ; MiddleName = None ; LastName = "Doe" ; Age = 34 ; IsActive = true } + static member ExpectedJson = """{"Name":"John","MiddleName":null,"LastName":"Doe","Age":34,"IsActive":true}""" + +[] +let ``Support F# struct record serialization``() = + let actualJson = JsonSerializer.Serialize(MyStructRecord.Value) + Assert.Equal(MyStructRecord.ExpectedJson, actualJson) + +[] +let ``Support F# struct record deserialization``() = + let result = JsonSerializer.Deserialize(MyStructRecord.ExpectedJson) + Assert.Equal(MyStructRecord.Value, result) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj new file mode 100644 index 00000000000000..2b5125a460b788 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj @@ -0,0 +1,14 @@ + + + + $(NetCoreAppCurrent) + + + + + + + + + + From 2e1d4491751e14b552064f0131bbe32629ccc08b Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Sat, 3 Jul 2021 02:32:49 +0100 Subject: [PATCH 02/12] fix ILLink warnings --- .../Converters/FSharp/FSharpListConverter.cs | 2 ++ .../Converters/FSharp/FSharpMapConverter.cs | 2 ++ .../FSharp/FSharpOptionConverter.cs | 2 ++ .../Converters/FSharp/FSharpSetConverter.cs | 2 ++ .../FSharp/FSharpTypeConverterFactory.cs | 6 ++++ .../Metadata/FSharpCoreReflectionProxy.cs | 35 +++++++++++++------ 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs index 4c4116bf02ca1b..036d04e8bebf19 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -12,6 +13,7 @@ internal sealed class FSharpListConverter : IEnumerableDefaultC { private readonly Func, TList> _listConstructor; + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] public FSharpListConverter() { _listConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpListConstructor(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs index 12d6d1bfd78c6e..30d16a09df84e2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -13,6 +14,7 @@ internal sealed class FSharpMapConverter : DictionaryDefault { private readonly Func>, TMap> _mapConstructor; + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] public FSharpMapConverter() { _mapConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpMapConstructor(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs index a11b5da88753e8..a666b717a02bc1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -18,6 +19,7 @@ internal sealed class FSharpOptionConverter : JsonResumableCo private readonly Func _optionValueGetter; private readonly Func _optionConstructor; + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] public FSharpOptionConverter(JsonConverter elementConverter) { _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionValueGetter(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs index 0597e80087f6b6..35c464643a3144 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -12,6 +13,7 @@ internal sealed class FSharpSetConverter : IEnumerableDefaultCon { private readonly Func, TSet> _setConstructor; + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] public FSharpSetConverter() { _setConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpSetConstructor(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs index 3cb3d315bd526f..1faa1fe3065e62 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs @@ -2,18 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters { + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] internal class FSharpTypeConverterFactory : JsonConverterFactory { private ObjectConverterFactory? _recordConverterFactory; + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked with RequiresUnreferencedCode.")] public override bool CanConvert(Type typeToConvert) => FSharpCoreReflectionProxy.IsFSharpType(typeToConvert) && FSharpCoreReflectionProxy.Instance.DetectFSharpKind(typeToConvert) is not FSharpKind.Unrecognized; + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked with RequiresUnreferencedCode.")] public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { Debug.Assert(CanConvert(typeToConvert)); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs index 0c5ad107ee51de..53bd7bf49dc11c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace System.Text.Json.Serialization.Metadata @@ -24,6 +25,8 @@ internal enum FSharpKind /// internal sealed class FSharpCoreReflectionProxy { + public const string FSharpCoreUnreferencedCodeMessage = "Uses Reflection to access FSharp.Core components, which requires unreferenced code."; + private static object s_lockObj = new object(); private static FSharpCoreReflectionProxy? s_singletonInstance; @@ -80,12 +83,15 @@ public static FSharpCoreReflectionProxy Instance } } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The public methods are marked RequiresUnreferencedCode.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "The public methods are marked RequiresUnreferencedCode.")] private FSharpCoreReflectionProxy(Assembly fsharpCoreAssembly) { Debug.Assert(fsharpCoreAssembly.GetName().Name == "FSharp.Core"); - _compilationMappingAttributeType = fsharpCoreAssembly.GetType(CompilationMappingAttributeTypeName)!; - _sourceConstructFlagsGetter = _compilationMappingAttributeType.GetProperty("SourceConstructFlags")?.GetGetMethod(); + Type compilationMappingAttributeType = fsharpCoreAssembly.GetType(CompilationMappingAttributeTypeName)!; + _sourceConstructFlagsGetter = compilationMappingAttributeType.GetMethod("get_SourceConstructFlags", BindingFlags.Public | BindingFlags.Instance); + _compilationMappingAttributeType = compilationMappingAttributeType; _fsharpOptionType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); _fsharpListType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpList`1"); @@ -97,6 +103,7 @@ private FSharpCoreReflectionProxy(Assembly fsharpCoreAssembly) _fsharpMapCtor = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.MapModule")?.GetMethod("OfSeq", BindingFlags.Public | BindingFlags.Static); } + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public FSharpKind DetectFSharpKind(Type type) { Attribute? compilationMappingAttribute = GetFSharpCompilationMappingAttribute(type); @@ -122,35 +129,43 @@ public FSharpKind DetectFSharpKind(Type type) }; } - public Func CreateFSharpOptionValueGetter() + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func CreateFSharpOptionValueGetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, T>() { - Debug.Assert(_fsharpOptionType!.MakeGenericType(typeof(T)) == typeof(TFSharpOption)); - MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpOption).GetProperty("Value", BindingFlags.Public | BindingFlags.Instance)?.GetGetMethod(), "Option.Value"); + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType!); + MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "Option.Value"); return CreateDelegate>(valueGetter); } - public Func CreateFSharpOptionConstructor() + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func CreateFSharpOptionConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, T>() { - Debug.Assert(_fsharpOptionType!.MakeGenericType(typeof(T)) == typeof(TFSharpOption)); + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType!); MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Option.Some"); return CreateDelegate>(methodInfo); } + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:MakeGenericMethod", Justification = "The public methods are marked RequiresUnreferencedCode.")] public Func, TFSharpList> CreateFSharpListConstructor() { - Debug.Assert(_fsharpListType!.MakeGenericType(typeof(T)) == typeof(TFSharpList)); + Debug.Assert(typeof(TFSharpList).GetGenericTypeDefinition() == _fsharpListType!); return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "List.ofSeq").MakeGenericMethod(typeof(T))); } + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:MakeGenericMethod", Justification = "The public methods are marked RequiresUnreferencedCode.")] public Func, TFSharpSet> CreateFSharpSetConstructor() { - Debug.Assert(_fsharpSetType!.MakeGenericType(typeof(T)) == typeof(TFSharpSet)); + Debug.Assert(typeof(TFSharpSet).GetGenericTypeDefinition() == _fsharpSetType!); return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Set.ofSeq").MakeGenericMethod(typeof(T))); } + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:MakeGenericMethod", Justification = "The public methods are marked RequiresUnreferencedCode.")] public Func>, TFSharpMap> CreateFSharpMapConstructor() { - Debug.Assert(_fsharpMapType!.MakeGenericType(typeof(TKey), typeof(TValue)) == typeof(TFSharpMap)); + Debug.Assert(typeof(TFSharpMap).GetGenericTypeDefinition() == _fsharpMapType!); return CreateDelegate>, TFSharpMap>>(EnsureMemberExists(_fsharpMapCtor, "Map.ofSeq").MakeGenericMethod(typeof(TKey), typeof(TValue))); } From d9cc505c6b6a771632053c1d5961678a900768f1 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 12 Jul 2021 08:32:35 +0100 Subject: [PATCH 03/12] address ILLink annotations feedback --- .../FSharp/FSharpTypeConverterFactory.cs | 7 ++++- .../Metadata/FSharpCoreReflectionProxy.cs | 30 ++++++++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs index 1faa1fe3065e62..b5f4259abb3f8f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs @@ -4,12 +4,17 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; +using FSharpKind = System.Text.Json.Serialization.Metadata.FSharpCoreReflectionProxy.FSharpKind; namespace System.Text.Json.Serialization.Converters { - [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] internal class FSharpTypeConverterFactory : JsonConverterFactory { + // Temporary solution to account for not implemented support for type-level attributes + // TODO remove once addressed https://github.com/mono/linker/issues/1742#issuecomment-875036480 + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpTypeConverterFactory() { } + private ObjectConverterFactory? _recordConverterFactory; [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs index 53bd7bf49dc11c..61dacba803b649 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -12,20 +12,25 @@ namespace System.Text.Json.Serialization.Metadata // Every F# application ships with a copy of FSharp.Core, however it is not available statically to System.Text.Json. // The following class uses reflection to access the relevant APIs required to detect the various F# types we are looking to support. - /// - /// The various categories of F# types that System.Text.Json supports. - /// - internal enum FSharpKind - { - Unrecognized, Option, List, Set, Map, Record - } - /// /// Proxy class used to access FSharp.Core metadata and reflection APIs that are not statically available to System.Text.Json. /// internal sealed class FSharpCoreReflectionProxy { - public const string FSharpCoreUnreferencedCodeMessage = "Uses Reflection to access FSharp.Core components, which requires unreferenced code."; + /// + /// The various categories of F# types that System.Text.Json supports. + /// + public enum FSharpKind + { + Unrecognized, + Option, + List, + Set, + Map, + Record + } + + public const string FSharpCoreUnreferencedCodeMessage = "Uses Reflection to access FSharp.Core components at runtime."; private static object s_lockObj = new object(); private static FSharpCoreReflectionProxy? s_singletonInstance; @@ -50,6 +55,7 @@ internal sealed class FSharpCoreReflectionProxy /// Checks if the provided System.Type instance is emitted by the F# compiler. /// If true, also initializes the proxy singleton for future by other F# types. /// + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public static bool IsFSharpType(Type type) { if (s_singletonInstance is null) @@ -83,8 +89,7 @@ public static FSharpCoreReflectionProxy Instance } } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The public methods are marked RequiresUnreferencedCode.")] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "The public methods are marked RequiresUnreferencedCode.")] + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] private FSharpCoreReflectionProxy(Assembly fsharpCoreAssembly) { Debug.Assert(fsharpCoreAssembly.GetName().Name == "FSharp.Core"); @@ -146,7 +151,6 @@ public FSharpKind DetectFSharpKind(Type type) } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:MakeGenericMethod", Justification = "The public methods are marked RequiresUnreferencedCode.")] public Func, TFSharpList> CreateFSharpListConstructor() { Debug.Assert(typeof(TFSharpList).GetGenericTypeDefinition() == _fsharpListType!); @@ -154,7 +158,6 @@ public Func, TFSharpList> CreateFSharpListConstructor, TFSharpSet> CreateFSharpSetConstructor() { Debug.Assert(typeof(TFSharpSet).GetGenericTypeDefinition() == _fsharpSetType!); @@ -162,7 +165,6 @@ public Func, TFSharpSet> CreateFSharpSetConstructor>, TFSharpMap> CreateFSharpMapConstructor() { Debug.Assert(typeof(TFSharpMap).GetGenericTypeDefinition() == _fsharpMapType!); From ed8a0a561b75ef51eb480c731570ec81c1709735 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 12 Jul 2021 12:48:57 +0100 Subject: [PATCH 04/12] add support for ValueOption --- .../src/System.Text.Json.csproj | 1 + .../FSharp/FSharpOptionConverter.cs | 9 +- .../FSharp/FSharpTypeConverterFactory.cs | 5 + .../FSharp/FSharpValueOptionConverter.cs | 71 ++++++++ .../Json/Serialization/JsonConverterOfT.cs | 10 +- .../JsonResumableConverterOfT.cs | 2 +- .../Metadata/FSharpCoreReflectionProxy.cs | 36 +++- .../OptionTests.fs | 8 +- .../System.Text.Json.FSharp.Tests.fsproj | 1 + .../ValueOptionTests.fs | 154 ++++++++++++++++++ 10 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 848d0ad891ffbd..7d428ffb19484c 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -138,6 +138,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs index a666b717a02bc1..2c7ee191bce4f4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs @@ -17,15 +17,15 @@ internal sealed class FSharpOptionConverter : JsonResumableCo internal override Type? ElementType => typeof(TElement); private readonly Func _optionValueGetter; - private readonly Func _optionConstructor; + private readonly Func _optionConstructor; [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] public FSharpOptionConverter(JsonConverter elementConverter) { _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionValueGetter(); - _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionConstructor(); + _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionSomeConstructor(); // If the element converter is value, this converter will also be writing values - // Set a flag to signal this fact to the covnerter infrastracture. + // Set a flag to signal this fact to the converter infrastructure. CanWriteJsonValues = elementConverter.ConverterStrategy == ConverterStrategy.Value; } @@ -36,8 +36,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, if (elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element)) { - // null element values are deserialized as 'None'. - value = element is null ? null : _optionConstructor(element); + value = _optionConstructor(element); return true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs index b5f4259abb3f8f..c8aa94abf82b3e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs @@ -40,6 +40,11 @@ public override bool CanConvert(Type typeToConvert) => converterFactoryType = typeof(FSharpOptionConverter<,>).MakeGenericType(typeToConvert, elementType); constructorArguments = new object[] { options.GetConverterInternal(elementType) }; break; + case FSharpKind.ValueOption: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpValueOptionConverter<,>).MakeGenericType(typeToConvert, elementType); + constructorArguments = new object[] { options.GetConverterInternal(elementType) }; + break; case FSharpKind.List: elementType = typeToConvert.GetGenericArguments()[0]; converterFactoryType = typeof(FSharpListConverter<,>).MakeGenericType(typeToConvert, elementType); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs new file mode 100644 index 00000000000000..d7b7b10f2ecb0d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + // Converter for F# struct optional values: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-fsharpvalueoption-1.html + internal sealed class FSharpValueOptionConverter : JsonResumableConverter + where TValueOption : struct, IEquatable + { + // While technically not implementing IEnumerable, F# optionals are effectively generic collections of at most one element. + internal override ConverterStrategy ConverterStrategy => ConverterStrategy.Enumerable; + internal override Type? ElementType => typeof(TElement); + + public override bool HandleNull => true; + + private readonly FSharpCoreReflectionProxy.StructGetter _optionValueGetter; + private readonly Func _optionConstructor; + + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpValueOptionConverter(JsonConverter elementConverter) + { + _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionValueGetter(); + _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionSomeConstructor(); + // If the element converter is value, this converter will also be writing values + // Set a flag to signal this fact to the converter infrastructure. + CanWriteJsonValues = elementConverter.ConverterStrategy == ConverterStrategy.Value; + } + + internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TValueOption value) + { + state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + var elementConverter = (JsonConverter)state.Current.JsonPropertyInfo.ConverterBase; + + // `null` values deserialize as `ValueNone` + if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null) + { + value = default; + return true; + } + + if (elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element)) + { + value = _optionConstructor(element); + return true; + } + + value = default; + return false; + } + + internal override bool OnTryWrite(Utf8JsonWriter writer, TValueOption value, JsonSerializerOptions options, ref WriteStack state) + { + if (value.Equals(default)) + { + // Write `ValueNone` values as null + writer.WriteNullValue(); + return true; + } + + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + var elementConverter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo.ConverterBase; + + TElement element = _optionValueGetter(ref value); + return elementConverter.TryWrite(writer, element, options, ref state); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 27ba9a8965f4c9..7076f5af6f2f7f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -551,10 +551,14 @@ internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, break; default: - - // A non-value converter (object or collection) should always have Start and End tokens. + // A non-value converter (object or collection) should always have Start and End tokens + // unless it supports handling null value reads. + if (!isValueConverter && !(HandleNullOnRead && tokenType == JsonTokenType.Null)) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } // A value converter should not make any reads. - if (!isValueConverter || reader.BytesConsumed != bytesConsumed) + else if (reader.BytesConsumed != bytesConsumed) { ThrowHelper.ThrowJsonException_SerializationConverterRead(this); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs index ccf639bdec1793..9e65250653e57a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs @@ -45,6 +45,6 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer } } - public sealed override bool HandleNull => false; + public override bool HandleNull => false; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs index 61dacba803b649..f313ce069766e3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -24,12 +24,16 @@ public enum FSharpKind { Unrecognized, Option, + ValueOption, List, Set, Map, Record } + // Binding a struct getter method to a delegate requires that the struct parameter is passed byref. + public delegate TResult StructGetter(ref TStruct @this) where TStruct : struct; + public const string FSharpCoreUnreferencedCodeMessage = "Uses Reflection to access FSharp.Core components at runtime."; private static object s_lockObj = new object(); @@ -43,6 +47,7 @@ public enum FSharpKind private readonly MethodInfo? _sourceConstructFlagsGetter; private readonly Type? _fsharpOptionType; + private readonly Type? _fsharpValueOptionType; private readonly Type? _fsharpListType; private readonly Type? _fsharpSetType; private readonly Type? _fsharpMapType; @@ -99,6 +104,7 @@ private FSharpCoreReflectionProxy(Assembly fsharpCoreAssembly) _compilationMappingAttributeType = compilationMappingAttributeType; _fsharpOptionType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); + _fsharpValueOptionType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Core.FSharpValueOption`1"); _fsharpListType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpList`1"); _fsharpSetType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpSet`1"); _fsharpMapType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpMap`2"); @@ -122,6 +128,7 @@ public FSharpKind DetectFSharpKind(Type type) { Type genericType = type.GetGenericTypeDefinition(); if (genericType == _fsharpOptionType) return FSharpKind.Option; + if (genericType == _fsharpValueOptionType) return FSharpKind.ValueOption; if (genericType == _fsharpListType) return FSharpKind.List; if (genericType == _fsharpSetType) return FSharpKind.Set; if (genericType == _fsharpMapType) return FSharpKind.Map; @@ -143,25 +150,42 @@ public FSharpKind DetectFSharpKind(Type type) } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] - public Func CreateFSharpOptionConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, T>() + public Func CreateFSharpOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() { Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType!); MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Option.Some"); - return CreateDelegate>(methodInfo); + return CreateDelegate>(methodInfo); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public StructGetter CreateFSharpValueOptionValueGetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpValueOption, TElement>() + where TFSharpValueOption : struct + { + Debug.Assert(typeof(TFSharpValueOption).GetGenericTypeDefinition() == _fsharpValueOptionType!); + MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpValueOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "ValueOption.Value"); + return CreateDelegate>(valueGetter); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func CreateFSharpValueOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() + { + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpValueOptionType!); + MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "ValueOption.ValueSome"); + return CreateDelegate>(methodInfo); } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] - public Func, TFSharpList> CreateFSharpListConstructor() + public Func, TFSharpList> CreateFSharpListConstructor() { Debug.Assert(typeof(TFSharpList).GetGenericTypeDefinition() == _fsharpListType!); - return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "List.ofSeq").MakeGenericMethod(typeof(T))); + return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "List.ofSeq").MakeGenericMethod(typeof(TElement))); } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] - public Func, TFSharpSet> CreateFSharpSetConstructor() + public Func, TFSharpSet> CreateFSharpSetConstructor() { Debug.Assert(typeof(TFSharpSet).GetGenericTypeDefinition() == _fsharpSetType!); - return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Set.ofSeq").MakeGenericMethod(typeof(T))); + return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Set.ofSeq").MakeGenericMethod(typeof(TElement))); } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs index 54796a5184883d..26657cf3de7e7c 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs @@ -59,7 +59,7 @@ let ``Some collection element should serialize as the payload`` (value : 'T) = Assert.Equal(expected, actual) [] -let ``Some of null sgetOptionalElementInputs`` () = +let ``Some of null should serialize as null`` () = let expected = "null" let actual = JsonSerializer.Serialize(Some null) Assert.Equal(expected, actual) @@ -93,19 +93,19 @@ let ``Root-level null should deserialize as None``(_ : 'T) = [] [] -let ``Null property getOptionalElementInputsne``(_ : 'T) = +let ``Null property should deserialize as None``(_ : 'T) = let actual = JsonSerializer.Deserialize<{| value : 'T option |}>("""{"value":null}""") Assert.Equal(None, actual.value) [] [] -let ``Missing propergetOptionalElementInputs None``(_ : 'T) = +let ``Missing property should deserialize as None``(_ : 'T) = let actual = JsonSerializer.Deserialize<{| value : 'T option |}>("{}") Assert.Equal(None, actual.value) [] [] -let ``Null element sgetOptionalElementInputse``(_ : 'T) = +let ``Null element should deserialize as None``(_ : 'T) = let expected = [Option<'T>.None] let actual = JsonSerializer.Deserialize<'T option []>("""[null]""") Assert.Equal(expected, actual) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj index 2b5125a460b788..fcf0d5c24675fd 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj @@ -7,6 +7,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs new file mode 100644 index 00000000000000..2bc3100b66c93a --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs @@ -0,0 +1,154 @@ +module System.Text.Json.Tests.FSharp.ValueOptionTests + +open System.Text.Json +open System.Text.Json.Tests.FSharp.Helpers +open Xunit + +let getOptionalElementInputs() = seq { + let wrap value = [| box value |] + + wrap 42 + wrap false + wrap "string" + wrap [|1..5|] + wrap (3,2) + wrap {| Name = "Mary" ; Age = 32 |} + wrap struct {| Name = "Mary" ; Age = 32 |} +} + +[] +[] +let ``Root-level ValueNone should serialize as null``(_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T voption>(ValueNone) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueNone property should serialize as null``(_ : 'T) = + let expected = """{"value":null}""" + let actual = JsonSerializer.Serialize {| value = ValueOption<'T>.ValueNone |} + Assert.Equal(expected, actual) + +[] +[] +let ``ValueNone collection element should serialize as null``(_ : 'T) = + let expected = """[null]""" + let actual = JsonSerializer.Serialize [| ValueOption<'T>.ValueNone |] + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level ValueSome should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Serialize(ValueSome value) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome property should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Serialize {| value = ValueSome value |} + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome collection element should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize [|value|] + let actual = JsonSerializer.Serialize [|ValueSome value|] + Assert.Equal(expected, actual) + +[] +let ``ValueSome of null should serialize as null`` () = + let expected = "null" + let actual = JsonSerializer.Serialize(ValueSome null) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome of ValueNone should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T voption voption>(ValueSome ValueNone) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome of ValueSome of ValueNone should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T voption voption voption>(ValueSome (ValueSome ValueNone)) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome of ValueSome of value should serialize as value`` (value : 'T) = + let expected = JsonSerializer.Serialize value + let actual = JsonSerializer.Serialize(ValueSome (ValueSome value)) + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level null should deserialize as ValueNone``(_ : 'T) = + let actual = JsonSerializer.Deserialize<'T voption>("null") + Assert.Equal(ValueNone, actual) + +[] +[] +let ``Null property should deserialize as ValueNone``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T voption |}>("""{"value":null}""") + Assert.Equal(ValueNone, actual.value) + +[] +[] +let ``Missing property should deserialize as ValueNone``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T voption |}>("{}") + Assert.Equal(ValueNone, actual.value) + +[] +[] +let ``Null element should deserialize as ValueNone``(_ : 'T) = + let expected = [ValueOption<'T>.ValueNone] + let actual = JsonSerializer.Deserialize<'T voption []>("""[null]""") + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level value should deserialize as ValueSome``(value : 'T) = + let json = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Deserialize<'T voption>(json) + Assert.Equal(ValueSome value, actual) + +[] +[] +let ``Property value should deserialize as ValueSome``(value : 'T) = + let json = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Deserialize<{| value : 'T voption|}>(json) + Assert.Equal(ValueSome value, actual.value) + +[] +[] +let ``Collection element should deserialize as ValueSome``(value : 'T) = + let json = JsonSerializer.Serialize [| value |] + let actual = JsonSerializer.Deserialize<'T voption []>(json) + Assert.Equal([ValueSome value], actual) + +[] +let ``Optional value should support resumable serialization``() = async { + let valueToSerialize = {| Values = ValueSome [|1 .. 200|] |} + let expectedJson = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(valueToSerialize, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Optional value should support resumable deserialization``() = async { + let valueToSerialize = {| Values = ValueSome [|1 .. 200|] |} + let json = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync<{| Values : int [] voption |}>(json, options) + + Assert.Equal(valueToSerialize, result) +} From e666c4f2f5732c72b958eb797a7eddbcabafdd89 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 13 Jul 2021 14:10:11 +0100 Subject: [PATCH 05/12] revert unneeded sln changes --- .../System.Text.Json/System.Text.Json.sln | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index a27abbe6bc9774..33dd38d2262a21 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -1,35 +1,32 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31430.445 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj", "{E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj", "{E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Win32.Registry", "..\Microsoft.Win32.Registry\ref\Microsoft.Win32.Registry.csproj", "{25C42754-B384-4842-8FA7-75D7A79ADF0D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Win32.Registry", "..\Microsoft.Win32.Registry\ref\Microsoft.Win32.Registry.csproj", "{25C42754-B384-4842-8FA7-75D7A79ADF0D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Collections.Immutable", "..\System.Collections.Immutable\src\System.Collections.Immutable.csproj", "{1C8262DB-7355-40A8-A2EC-4EED7363134A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Collections.Immutable", "..\System.Collections.Immutable\src\System.Collections.Immutable.csproj", "{1C8262DB-7355-40A8-A2EC-4EED7363134A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Pipelines", "..\System.IO.Pipelines\ref\System.IO.Pipelines.csproj", "{4774F56D-16A8-4ABB-8C73-5F57609F1773}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.IO.Pipelines", "..\System.IO.Pipelines\ref\System.IO.Pipelines.csproj", "{4774F56D-16A8-4ABB-8C73-5F57609F1773}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Pipelines", "..\System.IO.Pipelines\src\System.IO.Pipelines.csproj", "{D05FD93A-BC51-466E-BD56-3F3D6BBE6B06}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.IO.Pipelines", "..\System.IO.Pipelines\src\System.IO.Pipelines.csproj", "{D05FD93A-BC51-466E-BD56-3F3D6BBE6B06}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{E2077991-EB83-471C-B17F-72F569FFCE6D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{E2077991-EB83-471C-B17F-72F569FFCE6D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{7909EB27-0D6E-46E6-B9F9-8A1EFD557018}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{7909EB27-0D6E-46E6-B9F9-8A1EFD557018}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Security.AccessControl", "..\System.Security.AccessControl\ref\System.Security.AccessControl.csproj", "{BE230195-2A1C-4674-BACB-502C2CD864E9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Security.AccessControl", "..\System.Security.AccessControl\ref\System.Security.AccessControl.csproj", "{BE230195-2A1C-4674-BACB-502C2CD864E9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Security.Principal.Windows", "..\System.Security.Principal.Windows\ref\System.Security.Principal.Windows.csproj", "{D7276D7D-F117-47C5-B514-8E3E964769BE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Security.Principal.Windows", "..\System.Security.Principal.Windows\ref\System.Security.Principal.Windows.csproj", "{D7276D7D-F117-47C5-B514-8E3E964769BE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Encodings.Web", "..\System.Text.Encodings.Web\src\System.Text.Encodings.Web.csproj", "{9BCCDA15-8907-4AE3-8871-2F17775DDE4C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Encodings.Web", "..\System.Text.Encodings.Web\src\System.Text.Encodings.Web.csproj", "{9BCCDA15-8907-4AE3-8871-2F17775DDE4C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\System.Text.Json.csproj", "{7015E94D-D20D-48C8-86D7-6A996BE99E0E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json", "ref\System.Text.Json.csproj", "{7015E94D-D20D-48C8-86D7-6A996BE99E0E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3C544454-BD8B-44F4-A174-B61F18957613}" EndProject @@ -39,13 +36,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{676B6044-FA4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{74017ACD-3AC1-4BB5-804B-D57E305FFBD9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}" EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" EndProject From 12de895a52e66fdbe040b2848d53fa2a1b385c6a Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 13 Jul 2021 17:04:25 +0100 Subject: [PATCH 06/12] add JsonIgnoreCondition tests for optional types --- .../tests/System.Text.Json.FSharp.Tests/OptionTests.fs | 9 +++++++++ .../System.Text.Json.FSharp.Tests/ValueOptionTests.fs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs index 26657cf3de7e7c..fb5ed8a64b2070 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs @@ -1,6 +1,7 @@ module System.Text.Json.Tests.FSharp.OptionTests open System.Text.Json +open System.Text.Json.Serialization open System.Text.Json.Tests.FSharp.Helpers open Xunit @@ -85,6 +86,14 @@ let ``Some of Some of value should serialize as value`` (value : 'T) = let actual = JsonSerializer.Serialize(Some (Some value)) Assert.Equal(expected, actual) +[] +[] +let ``WhenWritingNull enabled should skip None properties``(_ : 'T) = + let expected = "{}" + let options = new JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull) + let actual = JsonSerializer.Serialize<{| value : 'T option |}>({| value = None |}, options) + Assert.Equal(expected, actual) + [] [] let ``Root-level null should deserialize as None``(_ : 'T) = diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs index 2bc3100b66c93a..7ff95286a33233 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs @@ -3,6 +3,7 @@ open System.Text.Json open System.Text.Json.Tests.FSharp.Helpers open Xunit +open System.Text.Json.Serialization let getOptionalElementInputs() = seq { let wrap value = [| box value |] @@ -85,6 +86,14 @@ let ``ValueSome of ValueSome of value should serialize as value`` (value : 'T) = let actual = JsonSerializer.Serialize(ValueSome (ValueSome value)) Assert.Equal(expected, actual) +[] +[] +let ``WhenWritingDefault enabled should skip ValueNone properties``(_ : 'T) = + let expected = "{}" + let options = new JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault) + let actual = JsonSerializer.Serialize<{| value : 'T voption |}>({| value = ValueNone |}, options) + Assert.Equal(expected, actual) + [] [] let ``Root-level null should deserialize as ValueNone``(_ : 'T) = From 967dd50faa5256aa7deb578fe2a706626911ba32 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 13 Jul 2021 17:25:10 +0100 Subject: [PATCH 07/12] Revert "revert unneeded sln changes" This reverts commit 2e793422dca84bd22d55cdfa2cd6c9b6c5d4963e. --- .../System.Text.Json/System.Text.Json.sln | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index 33dd38d2262a21..7e8af66bc59f81 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -1,32 +1,31 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj", "{E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj", "{E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Win32.Registry", "..\Microsoft.Win32.Registry\ref\Microsoft.Win32.Registry.csproj", "{25C42754-B384-4842-8FA7-75D7A79ADF0D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Win32.Registry", "..\Microsoft.Win32.Registry\ref\Microsoft.Win32.Registry.csproj", "{25C42754-B384-4842-8FA7-75D7A79ADF0D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Collections.Immutable", "..\System.Collections.Immutable\src\System.Collections.Immutable.csproj", "{1C8262DB-7355-40A8-A2EC-4EED7363134A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Collections.Immutable", "..\System.Collections.Immutable\src\System.Collections.Immutable.csproj", "{1C8262DB-7355-40A8-A2EC-4EED7363134A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.IO.Pipelines", "..\System.IO.Pipelines\ref\System.IO.Pipelines.csproj", "{4774F56D-16A8-4ABB-8C73-5F57609F1773}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Pipelines", "..\System.IO.Pipelines\ref\System.IO.Pipelines.csproj", "{4774F56D-16A8-4ABB-8C73-5F57609F1773}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.IO.Pipelines", "..\System.IO.Pipelines\src\System.IO.Pipelines.csproj", "{D05FD93A-BC51-466E-BD56-3F3D6BBE6B06}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Pipelines", "..\System.IO.Pipelines\src\System.IO.Pipelines.csproj", "{D05FD93A-BC51-466E-BD56-3F3D6BBE6B06}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{E2077991-EB83-471C-B17F-72F569FFCE6D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{E2077991-EB83-471C-B17F-72F569FFCE6D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{7909EB27-0D6E-46E6-B9F9-8A1EFD557018}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{7909EB27-0D6E-46E6-B9F9-8A1EFD557018}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Security.AccessControl", "..\System.Security.AccessControl\ref\System.Security.AccessControl.csproj", "{BE230195-2A1C-4674-BACB-502C2CD864E9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Security.AccessControl", "..\System.Security.AccessControl\ref\System.Security.AccessControl.csproj", "{BE230195-2A1C-4674-BACB-502C2CD864E9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Security.Principal.Windows", "..\System.Security.Principal.Windows\ref\System.Security.Principal.Windows.csproj", "{D7276D7D-F117-47C5-B514-8E3E964769BE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Security.Principal.Windows", "..\System.Security.Principal.Windows\ref\System.Security.Principal.Windows.csproj", "{D7276D7D-F117-47C5-B514-8E3E964769BE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Encodings.Web", "..\System.Text.Encodings.Web\src\System.Text.Encodings.Web.csproj", "{9BCCDA15-8907-4AE3-8871-2F17775DDE4C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Encodings.Web", "..\System.Text.Encodings.Web\src\System.Text.Encodings.Web.csproj", "{9BCCDA15-8907-4AE3-8871-2F17775DDE4C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json", "ref\System.Text.Json.csproj", "{7015E94D-D20D-48C8-86D7-6A996BE99E0E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\System.Text.Json.csproj", "{7015E94D-D20D-48C8-86D7-6A996BE99E0E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3C544454-BD8B-44F4-A174-B61F18957613}" EndProject @@ -36,13 +35,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{676B6044-FA4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{74017ACD-3AC1-4BB5-804B-D57E305FFBD9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}" EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" EndProject From e61a8da24c957e96e0f4f17a805eb4e62b0f9e82 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 13 Jul 2021 18:21:11 +0100 Subject: [PATCH 08/12] remove lock from singleton initialization --- .../Metadata/FSharpCoreReflectionProxy.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs index f313ce069766e3..fd6a309f6be110 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -36,7 +36,6 @@ public enum FSharpKind public const string FSharpCoreUnreferencedCodeMessage = "Uses Reflection to access FSharp.Core components at runtime."; - private static object s_lockObj = new object(); private static FSharpCoreReflectionProxy? s_singletonInstance; // Every type generated by the F# compiler is annotated with the CompilationMappingAttribute @@ -68,10 +67,7 @@ public static bool IsFSharpType(Type type) if (GetFSharpCoreAssembly(type) is Assembly fsharpCoreAssembly) { // Type is F# type, initialize the singleton instance. - lock (s_lockObj) - { - s_singletonInstance ??= new FSharpCoreReflectionProxy(fsharpCoreAssembly); - } + s_singletonInstance ??= new FSharpCoreReflectionProxy(fsharpCoreAssembly); return true; } @@ -144,7 +140,7 @@ public FSharpKind DetectFSharpKind(Type type) [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func CreateFSharpOptionValueGetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, T>() { - Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType!); + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType); MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "Option.Value"); return CreateDelegate>(valueGetter); } @@ -152,7 +148,7 @@ public FSharpKind DetectFSharpKind(Type type) [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func CreateFSharpOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() { - Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType!); + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType); MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Option.Some"); return CreateDelegate>(methodInfo); } @@ -161,7 +157,7 @@ public FSharpKind DetectFSharpKind(Type type) public StructGetter CreateFSharpValueOptionValueGetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpValueOption, TElement>() where TFSharpValueOption : struct { - Debug.Assert(typeof(TFSharpValueOption).GetGenericTypeDefinition() == _fsharpValueOptionType!); + Debug.Assert(typeof(TFSharpValueOption).GetGenericTypeDefinition() == _fsharpValueOptionType); MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpValueOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "ValueOption.Value"); return CreateDelegate>(valueGetter); } @@ -169,7 +165,7 @@ public FSharpKind DetectFSharpKind(Type type) [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func CreateFSharpValueOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() { - Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpValueOptionType!); + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpValueOptionType); MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "ValueOption.ValueSome"); return CreateDelegate>(methodInfo); } @@ -177,21 +173,21 @@ public FSharpKind DetectFSharpKind(Type type) [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func, TFSharpList> CreateFSharpListConstructor() { - Debug.Assert(typeof(TFSharpList).GetGenericTypeDefinition() == _fsharpListType!); + Debug.Assert(typeof(TFSharpList).GetGenericTypeDefinition() == _fsharpListType); return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "List.ofSeq").MakeGenericMethod(typeof(TElement))); } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func, TFSharpSet> CreateFSharpSetConstructor() { - Debug.Assert(typeof(TFSharpSet).GetGenericTypeDefinition() == _fsharpSetType!); + Debug.Assert(typeof(TFSharpSet).GetGenericTypeDefinition() == _fsharpSetType); return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Set.ofSeq").MakeGenericMethod(typeof(TElement))); } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func>, TFSharpMap> CreateFSharpMapConstructor() { - Debug.Assert(typeof(TFSharpMap).GetGenericTypeDefinition() == _fsharpMapType!); + Debug.Assert(typeof(TFSharpMap).GetGenericTypeDefinition() == _fsharpMapType); return CreateDelegate>, TFSharpMap>>(EnsureMemberExists(_fsharpMapCtor, "Map.ofSeq").MakeGenericMethod(typeof(TKey), typeof(TValue))); } From 4e68e18801fc70d8caa1d4f284b73c8137c2d57d Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 13 Jul 2021 18:36:25 +0100 Subject: [PATCH 09/12] improve FSharp.Core missing member error mesages --- .../System.Text.Json/src/Resources/Strings.resx | 2 +- .../Metadata/FSharpCoreReflectionProxy.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 5e059b3886813d..06ce2ce9837c9c 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -612,6 +612,6 @@ A 'field' member cannot be 'virtual'. See arguments for the '{0}' and '{1}' parameters. - Could not locate required member '{0}' from FSharp.Core. + Could not locate required member '{0}' from FSharp.Core. This might happen because your application has enabled member-level trimming. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs index fd6a309f6be110..3edbaa3c8244a9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -141,7 +141,7 @@ public FSharpKind DetectFSharpKind(Type type) public Func CreateFSharpOptionValueGetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, T>() { Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType); - MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "Option.Value"); + MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "Microsoft.FSharp.Core.FSharpOption.get_Value()"); return CreateDelegate>(valueGetter); } @@ -149,7 +149,7 @@ public FSharpKind DetectFSharpKind(Type type) public Func CreateFSharpOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() { Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType); - MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Option.Some"); + MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Microsoft.FSharp.Core.FSharpOption.Some(T value)"); return CreateDelegate>(methodInfo); } @@ -158,7 +158,7 @@ public FSharpKind DetectFSharpKind(Type type) where TFSharpValueOption : struct { Debug.Assert(typeof(TFSharpValueOption).GetGenericTypeDefinition() == _fsharpValueOptionType); - MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpValueOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "ValueOption.Value"); + MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpValueOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "Microsoft.FSharp.Core.FSharpValueOption.get_Value()"); return CreateDelegate>(valueGetter); } @@ -166,7 +166,7 @@ public FSharpKind DetectFSharpKind(Type type) public Func CreateFSharpValueOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() { Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpValueOptionType); - MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "ValueOption.ValueSome"); + MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Microsoft.FSharp.Core.FSharpValueOption.ValueSome(T value)"); return CreateDelegate>(methodInfo); } @@ -174,21 +174,21 @@ public FSharpKind DetectFSharpKind(Type type) public Func, TFSharpList> CreateFSharpListConstructor() { Debug.Assert(typeof(TFSharpList).GetGenericTypeDefinition() == _fsharpListType); - return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "List.ofSeq").MakeGenericMethod(typeof(TElement))); + return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "Microsoft.FSharp.Collections.ListModule.OfSeq(IEnumerable source)").MakeGenericMethod(typeof(TElement))); } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func, TFSharpSet> CreateFSharpSetConstructor() { Debug.Assert(typeof(TFSharpSet).GetGenericTypeDefinition() == _fsharpSetType); - return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Set.ofSeq").MakeGenericMethod(typeof(TElement))); + return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Microsoft.FSharp.Collections.SetModule.OfSeq(IEnumerable source)").MakeGenericMethod(typeof(TElement))); } [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] public Func>, TFSharpMap> CreateFSharpMapConstructor() { Debug.Assert(typeof(TFSharpMap).GetGenericTypeDefinition() == _fsharpMapType); - return CreateDelegate>, TFSharpMap>>(EnsureMemberExists(_fsharpMapCtor, "Map.ofSeq").MakeGenericMethod(typeof(TKey), typeof(TValue))); + return CreateDelegate>, TFSharpMap>>(EnsureMemberExists(_fsharpMapCtor, "Microsoft.FSharp.Collections.MapModule.OfSeq(IEnumerable> source)").MakeGenericMethod(typeof(TKey), typeof(TValue))); } private Attribute? GetFSharpCompilationMappingAttribute(Type type) From 74a58f3a1d8d0fcf810fb6d51b7720fbf30f32eb Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 13 Jul 2021 19:17:52 +0100 Subject: [PATCH 10/12] throw NotSupportedException on discriminated unions --- .../src/Resources/Strings.resx | 3 ++ .../FSharp/FSharpTypeConverterFactory.cs | 3 +- .../Metadata/FSharpCoreReflectionProxy.cs | 4 +- .../RecordTests.fs | 2 +- .../System.Text.Json.FSharp.Tests.fsproj | 1 + .../UnionTests.fs | 39 +++++++++++++++++++ .../ValueOptionTests.fs | 2 +- 7 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/UnionTests.fs diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 06ce2ce9837c9c..14f5e6b50a60c3 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -614,4 +614,7 @@ Could not locate required member '{0}' from FSharp.Core. This might happen because your application has enabled member-level trimming. + + F# discriminated union serialization is not supported. Consider authoring a custom converter for the type. + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs index c8aa94abf82b3e..c0cb9b88fd5743 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs @@ -64,7 +64,8 @@ public override bool CanConvert(Type typeToConvert) => ObjectConverterFactory objectFactory = _recordConverterFactory ??= new ObjectConverterFactory(useDefaultConstructorInUnannotatedStructs: false); Debug.Assert(objectFactory.CanConvert(typeToConvert)); return objectFactory.CreateConverter(typeToConvert, options); - + case FSharpKind.Union: + throw new NotSupportedException(SR.FSharpDiscriminatedUnionsNotSupported); default: Debug.Fail("Unrecognized F# type."); throw new Exception(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs index 3edbaa3c8244a9..a201c4b2fd9712 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -28,7 +28,8 @@ public enum FSharpKind List, Set, Map, - Record + Record, + Union } // Binding a struct getter method to a delegate requires that the struct parameter is passed byref. @@ -133,6 +134,7 @@ public FSharpKind DetectFSharpKind(Type type) return (GetSourceConstructFlags(compilationMappingAttribute) & SourceConstructFlags.KindMask) switch { SourceConstructFlags.RecordType => FSharpKind.Record, + SourceConstructFlags.SumType => FSharpKind.Union, _ => FSharpKind.Unrecognized }; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs index 638ec97add2632..0d78940904b52c 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs @@ -1,9 +1,9 @@ module System.Text.Json.Tests.FSharp.RecordTests open System.Text.Json +open System.Text.Json.Serialization open System.Text.Json.Tests.FSharp.Helpers open Xunit -open System.Text.Json.Serialization type MyRecord = { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj index fcf0d5c24675fd..3fef790dcfec62 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj @@ -10,6 +10,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/UnionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/UnionTests.fs new file mode 100644 index 00000000000000..d149ea6c27a7f1 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/UnionTests.fs @@ -0,0 +1,39 @@ +module System.Text.Json.Tests.FSharp.UnionTests + +open System +open System.Text.Json +open System.Text.Json.Serialization +open Xunit + +type MySingleCaseUnion = MySingleCaseUnion of string +type MyTypeSafeEnum = Label1 | Label2 | Label3 +type MyMultiCaseUnion = Point | Circle of radius:float | Rectangle of height:float * length:float + +[] +type MyStructSingleCaseUnion = MyStructSingleCaseUnion of string +[] +type MyStructTypeSafeEnum = StructLabel1 | StructLabel2 | StructLabel3 +[] +type MyStructMultiCaseUnion = StructPoint | StructCircle of radius:float | StructRectangle of height:float * length:float + +let getUnionValues() = seq { + let wrap value = [| value :> obj |] + + MySingleCaseUnion "value" |> wrap + Label1 |> wrap + Circle 1. |> wrap + + MyStructSingleCaseUnion "value" |> wrap + StructLabel2 |> wrap + StructCircle 1. |> wrap +} + +[] +[] +let ``Union serialization should throw NotSupportedException`` (value : 'T) = + Assert.Throws(fun () -> JsonSerializer.Serialize(value) |> ignore) + +[] +[] +let ``Union deserialization should throw NotSupportedException`` (value : 'T) = + Assert.Throws(fun () -> JsonSerializer.Deserialize<'T>("{}") |> ignore) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs index 7ff95286a33233..0744663256b782 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs @@ -1,9 +1,9 @@ module System.Text.Json.Tests.FSharp.ValueOptionTests open System.Text.Json +open System.Text.Json.Serialization open System.Text.Json.Tests.FSharp.Helpers open Xunit -open System.Text.Json.Serialization let getOptionalElementInputs() = seq { let wrap value = [| box value |] From 987e340776a89d3bb426b13fb8431fd3539c5788 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 14 Jul 2021 19:47:26 +0100 Subject: [PATCH 11/12] extend optional test coverage to include list, set and map payloads --- .../tests/System.Text.Json.FSharp.Tests/OptionTests.fs | 3 +++ .../tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs index fb5ed8a64b2070..ba97af58af7d69 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs @@ -15,6 +15,9 @@ let getOptionalElementInputs() = seq { wrap (3,2) wrap {| Name = "Mary" ; Age = 32 |} wrap struct {| Name = "Mary" ; Age = 32 |} + wrap [false; true; false; false] + wrap (Set.ofSeq [1 .. 5]) + wrap (Map.ofSeq [("key1", "value1"); ("key2", "value2")]) } [] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs index 0744663256b782..666ad64b8ecaa0 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs @@ -15,6 +15,9 @@ let getOptionalElementInputs() = seq { wrap (3,2) wrap {| Name = "Mary" ; Age = 32 |} wrap struct {| Name = "Mary" ; Age = 32 |} + wrap [false; true; false; false] + wrap (Set.ofSeq [1 .. 5]) + wrap (Map.ofSeq [("key1", "value1"); ("key2", "value2")]) } [] From 17a76a08d227049234b7f55046d56ae367951ae3 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 15 Jul 2021 08:46:10 +0100 Subject: [PATCH 12/12] simplify changes required to converter infrastructure --- .../FSharp/FSharpOptionConverter.cs | 71 +++++++++++++++---- .../FSharp/FSharpValueOptionConverter.cs | 61 ++++++++++++---- .../Text/Json/Serialization/JsonConverter.cs | 5 -- .../Json/Serialization/JsonConverterOfT.cs | 19 +++-- .../JsonResumableConverterOfT.cs | 2 +- .../Metadata/JsonPropertyInfoOfT.cs | 3 - .../Serialization/Metadata/JsonTypeInfo.cs | 7 +- 7 files changed, 114 insertions(+), 54 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs index 2c7ee191bce4f4..a02dd449a9da89 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs @@ -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.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; @@ -9,32 +8,45 @@ namespace System.Text.Json.Serialization.Converters { // Converter for F# optional values: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-option-1.html // Serializes `Some(value)` using the format of `value` and `None` values as `null`. - internal sealed class FSharpOptionConverter : JsonResumableConverter + internal sealed class FSharpOptionConverter : JsonConverter where TOption : class { - // While technically not implementing IEnumerable, F# optionals are effectively generic collections of at most one element. - internal override ConverterStrategy ConverterStrategy => ConverterStrategy.Enumerable; + // Reflect the converter strategy of the element type, since we use the identical contract for Some(_) values. + internal override ConverterStrategy ConverterStrategy => _converterStrategy; internal override Type? ElementType => typeof(TElement); + // 'None' is encoded using 'null' at runtime and serialized as 'null' in JSON. + public override bool HandleNull => true; + private readonly JsonConverter _elementConverter; private readonly Func _optionValueGetter; private readonly Func _optionConstructor; + private readonly ConverterStrategy _converterStrategy; [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] public FSharpOptionConverter(JsonConverter elementConverter) { + _elementConverter = elementConverter; _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionValueGetter(); _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionSomeConstructor(); - // If the element converter is value, this converter will also be writing values - // Set a flag to signal this fact to the converter infrastructure. - CanWriteJsonValues = elementConverter.ConverterStrategy == ConverterStrategy.Value; + + // temporary workaround for JsonConverter base constructor needing to access + // ConverterStrategy when calculating `CanUseDirectReadOrWrite`. + // TODO move `CanUseDirectReadOrWrite` from JsonConverter to JsonTypeInfo. + _converterStrategy = _elementConverter.ConverterStrategy; + CanUseDirectReadOrWrite = _converterStrategy == ConverterStrategy.Value; } internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TOption? value) { - state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; - var elementConverter = (JsonConverter)state.Current.JsonPropertyInfo.ConverterBase; + // `null` values deserialize as `None` + if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null) + { + value = null; + return true; + } - if (elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element)) + state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + if (_elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element)) { value = _optionConstructor(element); return true; @@ -46,12 +58,43 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, internal override bool OnTryWrite(Utf8JsonWriter writer, TOption value, JsonSerializerOptions options, ref WriteStack state) { - Debug.Assert(value is not null); // 'None' values are encoded as null: handled by the base converter. - state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; - var elementConverter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo.ConverterBase; + if (value is null) + { + // Write `None` values as null + writer.WriteNullValue(); + return true; + } TElement element = _optionValueGetter(value); - return elementConverter.TryWrite(writer, element, options, ref state); + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + return _elementConverter.TryWrite(writer, element, options, ref state); + } + + // Since this is a hybrid converter (ConverterStrategy depends on the element converter), + // we need to override the value converter Write and Read methods too. + + public override void Write(Utf8JsonWriter writer, TOption value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + } + else + { + TElement element = _optionValueGetter(value); + _elementConverter.Write(writer, element, options); + } + } + + public override TOption? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + TElement? element = _elementConverter.Read(ref reader, typeToConvert, options); + return _optionConstructor(element); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs index d7b7b10f2ecb0d..35d1640d88c4c3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs @@ -1,40 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters { // Converter for F# struct optional values: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-fsharpvalueoption-1.html - internal sealed class FSharpValueOptionConverter : JsonResumableConverter + // Serializes `ValueSome(value)` using the format of `value` and `ValueNone` values as `null`. + internal sealed class FSharpValueOptionConverter : JsonConverter where TValueOption : struct, IEquatable { - // While technically not implementing IEnumerable, F# optionals are effectively generic collections of at most one element. - internal override ConverterStrategy ConverterStrategy => ConverterStrategy.Enumerable; + // Reflect the converter strategy of the element type, since we use the identical contract for ValueSome(_) values. + internal override ConverterStrategy ConverterStrategy => _converterStrategy; internal override Type? ElementType => typeof(TElement); - + // 'ValueNone' is encoded using 'default' at runtime and serialized as 'null' in JSON. public override bool HandleNull => true; + private readonly JsonConverter _elementConverter; private readonly FSharpCoreReflectionProxy.StructGetter _optionValueGetter; private readonly Func _optionConstructor; + private readonly ConverterStrategy _converterStrategy; [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] public FSharpValueOptionConverter(JsonConverter elementConverter) { + _elementConverter = elementConverter; _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionValueGetter(); _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionSomeConstructor(); - // If the element converter is value, this converter will also be writing values - // Set a flag to signal this fact to the converter infrastructure. - CanWriteJsonValues = elementConverter.ConverterStrategy == ConverterStrategy.Value; + + // temporary workaround for JsonConverter base constructor needing to access + // ConverterStrategy when calculating `CanUseDirectReadOrWrite`. + // TODO move `CanUseDirectReadOrWrite` from JsonConverter to JsonTypeInfo. + _converterStrategy = _elementConverter.ConverterStrategy; + CanUseDirectReadOrWrite = _converterStrategy == ConverterStrategy.Value; } internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TValueOption value) { - state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; - var elementConverter = (JsonConverter)state.Current.JsonPropertyInfo.ConverterBase; - // `null` values deserialize as `ValueNone` if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null) { @@ -42,7 +45,8 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, return true; } - if (elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element)) + state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + if (_elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element)) { value = _optionConstructor(element); return true; @@ -61,11 +65,38 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, TValueOption value, Jso return true; } + TElement element = _optionValueGetter(ref value); + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; - var elementConverter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo.ConverterBase; + return _elementConverter.TryWrite(writer, element, options, ref state); + } - TElement element = _optionValueGetter(ref value); - return elementConverter.TryWrite(writer, element, options, ref state); + // Since this is a hybrid converter (ConverterStrategy depends on the element converter), + // we need to override the value converter Write and Read methods too. + + public override void Write(Utf8JsonWriter writer, TValueOption value, JsonSerializerOptions options) + { + if (value.Equals(default)) + { + // Write `ValueNone` values as null + writer.WriteNullValue(); + } + else + { + TElement element = _optionValueGetter(ref value); + _elementConverter.Write(writer, element, options); + } + } + + public override TValueOption Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + TElement? element = _elementConverter.Read(ref reader, typeToConvert, options); + return _optionConstructor(element); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index c51a2723b67c90..f324f672c915f7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -34,11 +34,6 @@ internal JsonConverter() { } internal bool CanBePolymorphic { get; set; } - /// - /// When set, indicates a non-value converter that is capable of writing or reading simple JSON values. - /// - internal bool CanWriteJsonValues { get; set; } - /// /// Used to support JsonObject as an extension property in a loosely-typed, trimmable manner. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 7076f5af6f2f7f..3a83812d3396e6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -249,8 +249,6 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali else #endif { - long originalBytesConsumed = 0; - if (!wasContinuation) { // For perf and converter simplicity, handle null here instead of forwarding to the converter. @@ -274,10 +272,6 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali Debug.Assert(state.Current.OriginalDepth == 0); state.Current.OriginalDepth = reader.CurrentDepth; - - // Do not store in the ReadStack since it will only be used if the converter - // behaves like a value converter (hence no continuations are expected to occur). - originalBytesConsumed = reader.BytesConsumed; } success = OnTryRead(ref reader, typeToConvert, options, ref state, out value); @@ -292,8 +286,8 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali VerifyRead( state.Current.OriginalTokenType, state.Current.OriginalDepth, - bytesConsumed: originalBytesConsumed, - isValueConverter: CanWriteJsonValues, + bytesConsumed: 0, + isValueConverter: false, ref reader); // No need to clear state.Current.* since a stack pop will occur. @@ -552,10 +546,13 @@ internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, default: // A non-value converter (object or collection) should always have Start and End tokens - // unless it supports handling null value reads. - if (!isValueConverter && !(HandleNullOnRead && tokenType == JsonTokenType.Null)) + if (!isValueConverter) { - ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + // with the exception of converters that support null value reads + if (!HandleNullOnRead || tokenType != JsonTokenType.Null) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } } // A value converter should not make any reads. else if (reader.BytesConsumed != bytesConsumed) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs index 9e65250653e57a..ccf639bdec1793 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs @@ -45,6 +45,6 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer } } - public override bool HandleNull => false; + public sealed override bool HandleNull => false; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index dda0e7f12dd1fe..28d244109f0d4e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -296,9 +296,6 @@ value is not null && if (Converter.HandleNullOnWrite) { - // No object, collection, or re-entrancy converter handles null. - Debug.Assert(Converter.ConverterStrategy == ConverterStrategy.Value); - if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 077de6d05372dc..956fefeee476c6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -49,9 +49,6 @@ internal JsonTypeInfo? ElementTypeInfo { if (_elementTypeInfo == null && ElementType != null) { - Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Enumerable || - PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Dictionary); - _elementTypeInfo = Options.GetOrAddClass(ElementType); } @@ -181,6 +178,8 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json PropertyInfoForTypeInfo = CreatePropertyInfoForTypeInfo(Type, runtimeType, converter, typeNumberHandling, Options); + ElementType = converter.ElementType; + switch (PropertyInfoForTypeInfo.ConverterStrategy) { case ConverterStrategy.Object: @@ -303,14 +302,12 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json break; case ConverterStrategy.Enumerable: { - ElementType = converter.ElementType; CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType); } break; case ConverterStrategy.Dictionary: { KeyType = converter.KeyType; - ElementType = converter.ElementType; CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType); } break;