From 0f76e31e72c366993fb473ada341178a75267e71 Mon Sep 17 00:00:00 2001 From: Joe Mayo Date: Fri, 1 May 2026 15:57:39 -0700 Subject: [PATCH] Upgrade YamlDotNet to 16.1.3 and remove workarounds no longer needed - Remove workarounds related to being able to deserialize nested objects, as it's now passed into converter APIs - Created `NamedObjectYamlConverter` to simplify generic type conversions - Created `NamedObjectMappingYamlConverter` to handle (de)serialization of `NamedObjectMapping{TValue}`, replacing the previous implementation of `IYamlConvertible` --- src/Directory.Packages.props | 2 +- src/PAModel/packages.lock.json | 18 ++--- .../NamedObjectMappingSerializationTests.cs | 14 ++++ .../NamedObjectSequenceSerializationTests.cs | 8 +-- .../Serialization/SerializationTestBase.cs | 3 - .../PaYaml/Models/NamedObjectMapping.cs | 14 ---- .../PaYaml/Models/NamedObjectMappingBase.cs | 61 +---------------- .../PaYaml/Models/PaYamlLocation.cs | 2 +- .../NamedObjectMappingYamlConverter.cs | 43 ++++++++++++ .../NamedObjectMappingYamlConverterOfT.cs | 62 +++++++++++++++++ .../Serialization/NamedObjectYamlConverter.cs | 67 ++++++------------- .../NamedObjectYamlConverterOfT.cs | 65 ++++++++++++++++++ .../PFxExpressionYamlConverter.cs | 13 ++-- .../PaYamlSerializationContext.cs | 46 +++---------- .../PaYaml/Serialization/PaYamlSerializer.cs | 5 -- .../PaYaml/Serialization/YamlConverterOfT.cs | 19 ++---- .../Serialization/YamlDotNetExtensions.cs | 2 +- src/Persistence/PersistenceErrorCode.cs | 1 + src/Persistence/packages.lock.json | 18 ++--- 19 files changed, 251 insertions(+), 212 deletions(-) create mode 100644 src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverter.cs create mode 100644 src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverterOfT.cs create mode 100644 src/Persistence/PaYaml/Serialization/NamedObjectYamlConverterOfT.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 1c44b45d..daf3c7fd 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -31,7 +31,7 @@ - + diff --git a/src/PAModel/packages.lock.json b/src/PAModel/packages.lock.json index 0464a0b4..7b4250db 100644 --- a/src/PAModel/packages.lock.json +++ b/src/PAModel/packages.lock.json @@ -42,9 +42,9 @@ }, "YamlDotNet": { "type": "Direct", - "requested": "[15.1.6, )", - "resolved": "15.1.6", - "contentHash": "T/cQEK/KHK96Q8kytJ4iUGDXg1/fj2Qtk6rCQeIlHYU1zTeyGVHW0QNZgREQyxZpygGMDMmrXNWt0sj5TsQnjA==" + "requested": "[16.1.3, )", + "resolved": "16.1.3", + "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", @@ -116,9 +116,9 @@ }, "YamlDotNet": { "type": "Direct", - "requested": "[15.1.6, )", - "resolved": "15.1.6", - "contentHash": "T/cQEK/KHK96Q8kytJ4iUGDXg1/fj2Qtk6rCQeIlHYU1zTeyGVHW0QNZgREQyxZpygGMDMmrXNWt0sj5TsQnjA==" + "requested": "[16.1.3, )", + "resolved": "16.1.3", + "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" } }, "net8.0": { @@ -142,9 +142,9 @@ }, "YamlDotNet": { "type": "Direct", - "requested": "[15.1.6, )", - "resolved": "15.1.6", - "contentHash": "T/cQEK/KHK96Q8kytJ4iUGDXg1/fj2Qtk6rCQeIlHYU1zTeyGVHW0QNZgREQyxZpygGMDMmrXNWt0sj5TsQnjA==" + "requested": "[16.1.3, )", + "resolved": "16.1.3", + "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" } } } diff --git a/src/Persistence.Tests/PaYaml/Serialization/NamedObjectMappingSerializationTests.cs b/src/Persistence.Tests/PaYaml/Serialization/NamedObjectMappingSerializationTests.cs index d4d2c89e..58cace34 100644 --- a/src/Persistence.Tests/PaYaml/Serialization/NamedObjectMappingSerializationTests.cs +++ b/src/Persistence.Tests/PaYaml/Serialization/NamedObjectMappingSerializationTests.cs @@ -2,12 +2,26 @@ // Licensed under the MIT License. using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; +using YamlDotNet.Serialization; namespace Persistence.Tests.PaYaml.Serialization; [TestClass] public class NamedObjectMappingSerializationTests : SerializationTestBase { + protected override void ConfigureYamlDotNetDeserializer(DeserializerBuilder builder, PaYamlSerializationContext context) + { + base.ConfigureYamlDotNetDeserializer(builder, context); + builder.WithTypeConverter(new NamedObjectMappingYamlConverter()); + } + + protected override void ConfigureYamlDotNetSerializer(SerializerBuilder builder, PaYamlSerializationContext context) + { + base.ConfigureYamlDotNetSerializer(builder, context); + builder.WithTypeConverter(new NamedObjectMappingYamlConverter()); + } + [TestMethod] // Null literals [DataRow("TheMapping: ~", null)] diff --git a/src/Persistence.Tests/PaYaml/Serialization/NamedObjectSequenceSerializationTests.cs b/src/Persistence.Tests/PaYaml/Serialization/NamedObjectSequenceSerializationTests.cs index 4f979c9f..686fb4c4 100644 --- a/src/Persistence.Tests/PaYaml/Serialization/NamedObjectSequenceSerializationTests.cs +++ b/src/Persistence.Tests/PaYaml/Serialization/NamedObjectSequenceSerializationTests.cs @@ -13,13 +13,13 @@ public class NamedObjectSequenceSerializationTests : SerializationTestBase protected override void ConfigureYamlDotNetDeserializer(DeserializerBuilder builder, PaYamlSerializationContext context) { base.ConfigureYamlDotNetDeserializer(builder, context); - builder.WithTypeConverter(new NamedObjectYamlConverter(context)); + builder.WithTypeConverter(new NamedObjectYamlConverter()); } protected override void ConfigureYamlDotNetSerializer(SerializerBuilder builder, PaYamlSerializationContext context) { base.ConfigureYamlDotNetSerializer(builder, context); - builder.WithTypeConverter(new NamedObjectYamlConverter(context)); + builder.WithTypeConverter(new NamedObjectYamlConverter()); } [TestMethod] @@ -75,7 +75,7 @@ public void ReadYamlMappingSetsNamedObjectStart() var testObject = DeserializeViaYamlDotNet>(yaml); testObject.ShouldNotBeNull(); testObject.TheSequence.ShouldNotBeNull(); - testObject.TheSequence.Names.Should().Equal(new[] { "n1", "n3", "n2" }, "ordering of a sequence is by code order"); + testObject.TheSequence.Names.Should().Equal(["n1", "n3", "n2"], "ordering of a sequence is by code order"); testObject.TheSequence.GetNamedObject("n1").Should() .HaveValueEqual("v1") .And.HaveStartEqual(2, 5); @@ -92,7 +92,7 @@ public void SerializeAsPropertyBeingNullOrEmpty() { SerializeViaYamlDotNet(new TestOM { TheSequence = null }) .Should().Be("{}" + DefaultOptions.NewLine); - SerializeViaYamlDotNet(new TestOM { TheSequence = new() }) + SerializeViaYamlDotNet(new TestOM { TheSequence = [] }) .Should().Be("{}" + DefaultOptions.NewLine); } diff --git a/src/Persistence.Tests/PaYaml/Serialization/SerializationTestBase.cs b/src/Persistence.Tests/PaYaml/Serialization/SerializationTestBase.cs index 3f23f310..e074229a 100644 --- a/src/Persistence.Tests/PaYaml/Serialization/SerializationTestBase.cs +++ b/src/Persistence.Tests/PaYaml/Serialization/SerializationTestBase.cs @@ -21,7 +21,6 @@ protected string SerializeViaYamlDotNet(T? testObject, PaYamlSerializerOption using var serializationContext = new PaYamlSerializationContext(options); var builder = new SerializerBuilder(); ConfigureYamlDotNetSerializer(builder, serializationContext); - serializationContext.ValueSerializer = builder.BuildValueSerializer(); var serializer = builder.Build(); return serializer.Serialize(testObject); @@ -40,11 +39,9 @@ protected virtual void ConfigureYamlDotNetSerializer(SerializerBuilder builder, using var serializationContext = new PaYamlSerializationContext(options); var builder = new DeserializerBuilder(); ConfigureYamlDotNetDeserializer(builder, serializationContext); - serializationContext.ValueDeserializer = builder.BuildValueDeserializer(); var deserializer = builder.Build(); var value = deserializer.Deserialize(yaml); - serializationContext.OnDeserialization(); return value; } diff --git a/src/Persistence/PaYaml/Models/NamedObjectMapping.cs b/src/Persistence/PaYaml/Models/NamedObjectMapping.cs index b0ba4a99..0d65a4de 100644 --- a/src/Persistence/PaYaml/Models/NamedObjectMapping.cs +++ b/src/Persistence/PaYaml/Models/NamedObjectMapping.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; -using YamlDotNet.Core; -using YamlDotNet.Serialization; - namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; /// @@ -42,14 +38,4 @@ protected override NamedObject CreateNamedObject(string name, TValue val return new NamedObject(name, value); } - - protected override NamedObject ReadNamedObjectFromMappingEntryEvents(IParser parser, ObjectDeserializer nestedObjectDeserializer) - { - return NamedObjectYamlConverter.ReadNameAndValueEventsCore(parser, nestedObjectDeserializer); - } - - protected override void WriteNamedObjectToMappingEntryEvents(IEmitter emitter, NamedObject namedObject, ObjectSerializer nestedObjectSerializer) - { - NamedObjectYamlConverter.WriteNameAndValueEventsCore(emitter, namedObject, nestedObjectSerializer); - } } diff --git a/src/Persistence/PaYaml/Models/NamedObjectMappingBase.cs b/src/Persistence/PaYaml/Models/NamedObjectMappingBase.cs index 3f5db8da..e167942d 100644 --- a/src/Persistence/PaYaml/Models/NamedObjectMappingBase.cs +++ b/src/Persistence/PaYaml/Models/NamedObjectMappingBase.cs @@ -3,17 +3,13 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; /// /// Base implementation for an yaml mapping. /// -public abstract class NamedObjectMappingBase : INamedObjectCollection, IYamlConvertible +public abstract class NamedObjectMappingBase : INamedObjectCollection where TName : notnull where TValue : notnull where TNamedObject : INamedObject @@ -157,59 +153,4 @@ public bool TryGetValue(TName name, [MaybeNullWhen(false)] out TValue value) return false; } } - - #region IYamlConvertible - - private void Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) - { - Debug.Assert(expectedType.IsAssignableTo(typeof(NamedObjectMappingBase))); - - if (parser.TryConsumeNull()) - { - // REVIEW: We may not want to support null scalars when reading named object mappings. - // This will require us to use a custom converter to be able to have access to return a null value. - // For now, we think all uses would be benign currently to just treat null inputs as an empty collection: - return; - } - - parser.Consume(); - while (!parser.TryConsume(out _)) - { - var itemStartEvent = parser.Current!; - var namedObject = ReadNamedObjectFromMappingEntryEvents(parser, nestedObjectDeserializer); - - if (!TryAdd(namedObject)) - { - var existingNamedObject = GetNamedObject(namedObject.Name); - throw new YamlException(itemStartEvent.Start, itemStartEvent.End, $"Duplicate name '{namedObject.Name}' used at {itemStartEvent}. First use is located at {existingNamedObject.Start}."); - } - } - } - - private void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) - { - emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, isImplicit: true, MappingStyle.Block)); - foreach (var namedObject in InnerCollection.Values) - { - WriteNamedObjectToMappingEntryEvents(emitter, namedObject, nestedObjectSerializer); - } - - emitter.Emit(new MappingEnd()); - } - - protected abstract TNamedObject ReadNamedObjectFromMappingEntryEvents(IParser parser, ObjectDeserializer nestedObjectDeserializer); - - protected abstract void WriteNamedObjectToMappingEntryEvents(IEmitter emitter, TNamedObject namedObject, ObjectSerializer nestedObjectSerializer); - - void IYamlConvertible.Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) - { - Read(parser, expectedType, nestedObjectDeserializer); - } - - void IYamlConvertible.Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) - { - Write(emitter, nestedObjectSerializer); - } - - #endregion } diff --git a/src/Persistence/PaYaml/Models/PaYamlLocation.cs b/src/Persistence/PaYaml/Models/PaYamlLocation.cs index d703f8a5..1956bb16 100644 --- a/src/Persistence/PaYaml/Models/PaYamlLocation.cs +++ b/src/Persistence/PaYaml/Models/PaYamlLocation.cs @@ -5,7 +5,7 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; -public record PaYamlLocation(int Line, int Column) +public record PaYamlLocation(long Line, long Column) { internal static PaYamlLocation? FromMark(Mark mark) { diff --git a/src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverter.cs b/src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverter.cs new file mode 100644 index 00000000..6c6d1759 --- /dev/null +++ b/src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; +using YamlDotNet.Core; +using YamlDotNet.Serialization; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; + +/// +/// Converts any to and from YAML. +/// The converter is non-generic so a single registration handles all closed TValue types. +/// +internal sealed class NamedObjectMappingYamlConverter : IYamlTypeConverter +{ + private static readonly ConcurrentDictionary ConverterCache = new(); + + public bool Accepts(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(NamedObjectMapping<>); + } + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + return GetConverter(type).ReadYaml(parser, type, rootDeserializer); + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + GetConverter(type).WriteYaml(emitter, value, type, serializer); + } + + private static IYamlTypeConverter GetConverter(Type closedMappingType) + { + return ConverterCache.GetOrAdd(closedMappingType, static t => + { + var valueType = t.GetGenericArguments()[0]; + var converterType = typeof(NamedObjectMappingYamlConverter<>).MakeGenericType(valueType); + return (IYamlTypeConverter)Activator.CreateInstance(converterType)!; + }); + } +} diff --git a/src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverterOfT.cs b/src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverterOfT.cs new file mode 100644 index 00000000..792f88e7 --- /dev/null +++ b/src/Persistence/PaYaml/Serialization/NamedObjectMappingYamlConverterOfT.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; + +/// +/// Strongly-typed converter for . +/// Typically discovered and dispatched to by the non-generic +/// , but can also be registered directly. +/// +internal sealed class NamedObjectMappingYamlConverter : YamlConverter> + where TValue : notnull +{ + public override NamedObjectMapping ReadYaml(IParser parser, Type typeToConvert, ObjectDeserializer rootDeserializer) + { + var mapping = new NamedObjectMapping(); + + if (parser.TryConsumeNull()) + { + // REVIEW: We may not want to support null scalars when reading named object mappings. + // For now, treat null inputs as an empty collection (matches prior IYamlConvertible behavior). + return mapping; + } + + parser.Consume(); + while (!parser.TryConsume(out _)) + { + var itemStartEvent = parser.Current!; + var namedObject = NamedObjectYamlConverter.ReadNameAndValueEventsCore(parser, rootDeserializer); + + if (!mapping.TryAdd(namedObject)) + { + var existingNamedObject = mapping.GetNamedObject(namedObject.Name); + throw new YamlException(itemStartEvent.Start, itemStartEvent.End, $"Duplicate name '{namedObject.Name}' used at {itemStartEvent}. First use is located at {existingNamedObject.Start}."); + } + } + + return mapping; + } + + public override void WriteYaml(IEmitter emitter, NamedObjectMapping? value, Type typeToConvert, ObjectSerializer serializer) + { + if (value is null) + { + emitter.EmitNull(); + return; + } + + emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, isImplicit: true, MappingStyle.Block)); + foreach (var namedObject in value) + { + NamedObjectYamlConverter.WriteNameAndValueEventsCore(emitter, namedObject, serializer); + } + + emitter.Emit(new MappingEnd()); + } +} diff --git a/src/Persistence/PaYaml/Serialization/NamedObjectYamlConverter.cs b/src/Persistence/PaYaml/Serialization/NamedObjectYamlConverter.cs index 9d960103..f9f9677d 100644 --- a/src/Persistence/PaYaml/Serialization/NamedObjectYamlConverter.cs +++ b/src/Persistence/PaYaml/Serialization/NamedObjectYamlConverter.cs @@ -1,70 +1,43 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using YamlDotNet.Core; -using YamlDotNet.Core.Events; +using System.Collections.Concurrent; using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; +using YamlDotNet.Core; using YamlDotNet.Serialization; namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; -internal class NamedObjectYamlConverter : YamlConverter> - where TValue : notnull +/// +/// Converts any to and from YAML. +/// The converter is non-generic so a single registration handles all closed TValue types. +/// +internal sealed class NamedObjectYamlConverter : IYamlTypeConverter { - public NamedObjectYamlConverter(PaYamlSerializationContext context) - : base(context) - { - } + private static readonly ConcurrentDictionary ConverterCache = new(); - internal static NamedObject ReadNameAndValueEventsCore(IParser parser, ObjectDeserializer nestedObjectDeserializer) + public bool Accepts(Type type) { - _ = parser.Current ?? throw new InvalidOperationException("The parser has not been started or has nothing to read."); - _ = nestedObjectDeserializer ?? throw new ArgumentNullException(nameof(nestedObjectDeserializer)); - - // The default representation is expected to represent a single-item mapping - var start = parser.Current.Start; - var name = (string?)nestedObjectDeserializer(typeof(string)) ?? throw new YamlException(start, parser.Current.End, $"Named object key cannot be null."); - var value = (TValue?)nestedObjectDeserializer(typeof(TValue)) ?? throw new YamlException(start, parser.Current.End, $"Named object value cannot be null."); - - return new NamedObject(name, value) { Start = PaYamlLocation.FromMark(start) }; + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(NamedObject<>); } - internal static void WriteNameAndValueEventsCore(IEmitter emitter, NamedObject namedObject, ObjectSerializer nestedObjectSerializer) + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { - _ = emitter ?? throw new ArgumentNullException(nameof(emitter)); - _ = namedObject ?? throw new ArgumentNullException(nameof(namedObject)); - _ = nestedObjectSerializer ?? throw new ArgumentNullException(nameof(nestedObjectSerializer)); - - // Only write the events for the mapping key/value. - nestedObjectSerializer(namedObject.Name, typeof(string)); - nestedObjectSerializer(namedObject.Value, typeof(TValue)); + return GetConverter(type).ReadYaml(parser, type, rootDeserializer); } - public override NamedObject ReadYaml(IParser parser, Type typeToConvert) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { - // The default representation is expected to represent a single-item mapping - var mappingStart = parser.Consume(); - var namedObject = ReadNameAndValueEventsCore(parser, SerializationContext.CreateObjectDeserializer(parser)); - - // There shouldn't be any more keys in the mapping - parser.Consume(); - - return namedObject; + GetConverter(type).WriteYaml(emitter, value, type, serializer); } - public override void WriteYaml(IEmitter emitter, NamedObject? value, Type typeToConvert) + private static IYamlTypeConverter GetConverter(Type closedNamedObjectType) { - if (value is null) + return ConverterCache.GetOrAdd(closedNamedObjectType, static t => { - emitter.EmitNull(); - return; - } - - // The default representation is expected to represent a single-item mapping - emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, isImplicit: true, MappingStyle.Block)); - - WriteNameAndValueEventsCore(emitter, value, SerializationContext.CreateObjectSerializer(emitter)); - - emitter.Emit(new MappingEnd()); + var valueType = t.GetGenericArguments()[0]; + var converterType = typeof(NamedObjectYamlConverter<>).MakeGenericType(valueType); + return (IYamlTypeConverter)Activator.CreateInstance(converterType)!; + }); } } diff --git a/src/Persistence/PaYaml/Serialization/NamedObjectYamlConverterOfT.cs b/src/Persistence/PaYaml/Serialization/NamedObjectYamlConverterOfT.cs new file mode 100644 index 00000000..c145bb3f --- /dev/null +++ b/src/Persistence/PaYaml/Serialization/NamedObjectYamlConverterOfT.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; +using YamlDotNet.Serialization; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; + +internal class NamedObjectYamlConverter : YamlConverter> + where TValue : notnull +{ + internal static NamedObject ReadNameAndValueEventsCore(IParser parser, ObjectDeserializer rootDeserializer) + { + _ = parser.Current ?? throw new InvalidOperationException("The parser has not been started or has nothing to read."); + _ = rootDeserializer ?? throw new ArgumentNullException(nameof(rootDeserializer)); + + // The default representation is expected to represent a single-item mapping + var start = parser.Current.Start; + var name = (string?)rootDeserializer(typeof(string)) ?? throw new YamlException(start, parser.Current.End, $"Named object key cannot be null."); + var value = (TValue?)rootDeserializer(typeof(TValue)) ?? throw new YamlException(start, parser.Current.End, $"Named object value cannot be null."); + + return new NamedObject(name, value) { Start = PaYamlLocation.FromMark(start) }; + } + + internal static void WriteNameAndValueEventsCore(IEmitter emitter, NamedObject namedObject, ObjectSerializer serializer) + { + _ = emitter ?? throw new ArgumentNullException(nameof(emitter)); + _ = namedObject ?? throw new ArgumentNullException(nameof(namedObject)); + _ = serializer ?? throw new ArgumentNullException(nameof(serializer)); + + // Only write the events for the mapping key/value. + serializer(namedObject.Name, typeof(string)); + serializer(namedObject.Value, typeof(TValue)); + } + + public override NamedObject ReadYaml(IParser parser, Type typeToConvert, ObjectDeserializer rootDeserializer) + { + // The default representation is expected to represent a single-item mapping + var mappingStart = parser.Consume(); + var namedObject = ReadNameAndValueEventsCore(parser, rootDeserializer); + + // There shouldn't be any more keys in the mapping + parser.Consume(); + + return namedObject; + } + + public override void WriteYaml(IEmitter emitter, NamedObject? value, Type typeToConvert, ObjectSerializer serializer) + { + if (value is null) + { + emitter.EmitNull(); + return; + } + + // The default representation is expected to represent a single-item mapping + emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, isImplicit: true, MappingStyle.Block)); + + WriteNameAndValueEventsCore(emitter, value, serializer); + + emitter.Emit(new MappingEnd()); + } +} diff --git a/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs b/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs index 515f39d1..47c46373 100644 --- a/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs +++ b/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs @@ -8,23 +8,18 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; -internal class PFxExpressionYamlConverter : IYamlTypeConverter +internal class PFxExpressionYamlConverter(PFxExpressionYamlFormattingOptions formattingOptions) : IYamlTypeConverter { private static readonly char[] LineTerminators = ['\r', '\n', '\x85', '\x2028', '\x2029']; - private readonly PFxExpressionYamlFormattingOptions _formattingOptions; - - public PFxExpressionYamlConverter(PFxExpressionYamlFormattingOptions formattingOptions) - { - _formattingOptions = formattingOptions ?? throw new ArgumentNullException(nameof(formattingOptions)); - } + private readonly PFxExpressionYamlFormattingOptions _formattingOptions = formattingOptions; public bool Accepts(Type type) { return type == typeof(PFxExpressionYaml); } - public object? ReadYaml(IParser parser, Type type) + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer _) { if (parser.TryConsumeNull()) return null; @@ -41,7 +36,7 @@ public bool Accepts(Type type) throw new YamlException(scalar.Start, scalar.End, $"Power Fx expressions must start with '{PFxExpressionYamlFormattingOptions.ScalarPrefix}'."); } - public void WriteYaml(IEmitter emitter, object? value, Type type) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer _) { var expression = (PFxExpressionYaml?)value; if (expression is null) diff --git a/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs b/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs index 1e2db7ef..e1d0ac6b 100644 --- a/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs +++ b/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs @@ -1,46 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3; -using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -using YamlDotNet.Serialization.Utilities; namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; -public class PaYamlSerializationContext(PaYamlSerializerOptions options) : IDisposable +public class PaYamlSerializationContext : IDisposable { - private readonly SerializerState _serializerState = new(); private bool _isDisposed; - /// - /// The options used when creating this context. - /// - public PaYamlSerializerOptions Options { get; } = options ?? throw new ArgumentNullException(nameof(options)); - - internal IValueSerializer? ValueSerializer { get; set; } - - internal IValueDeserializer? ValueDeserializer { get; set; } - - public ObjectDeserializer CreateObjectDeserializer(IParser parser) - { - var valueDeserializer = ValueDeserializer ?? throw new InvalidOperationException($"{nameof(ValueDeserializer)} is not set."); - - return (t) => valueDeserializer.DeserializeValue(parser, t, _serializerState, valueDeserializer); - } - - public ObjectSerializer CreateObjectSerializer(IEmitter emitter) + public PaYamlSerializationContext(PaYamlSerializerOptions options) { - var valueSerializer = ValueSerializer ?? throw new InvalidOperationException($"{nameof(ValueSerializer)} is not set."); - - return (v, t) => valueSerializer.SerializeValue(emitter, v, t); + Options = options ?? throw new ArgumentNullException(nameof(options)); } - internal void OnDeserialization() - { - _serializerState.OnDeserialization(); - } + /// + /// The options used when creating this context. + /// + public PaYamlSerializerOptions Options { get; } internal void ApplyToDeserializerBuilder(DeserializerBuilder builder) { @@ -48,6 +26,7 @@ internal void ApplyToDeserializerBuilder(DeserializerBuilder builder) .WithDuplicateKeyChecking() .IgnoreFields() ; + AddTypeConverters(builder); Options.AdditionalDeserializerConfiguration?.Invoke(builder); } @@ -76,8 +55,8 @@ private void AddTypeConverters(BuilderSkeleton builder) where TBuilder : BuilderSkeleton { builder.WithTypeConverter(new PFxExpressionYamlConverter(Options.PFxExpressionYamlFormatting)); - builder.WithTypeConverter(new NamedObjectYamlConverter(this)); - builder.WithTypeConverter(new NamedObjectYamlConverter(this)); + builder.WithTypeConverter(new NamedObjectYamlConverter()); + builder.WithTypeConverter(new NamedObjectMappingYamlConverter()); } /// @@ -93,11 +72,6 @@ protected virtual void Dispose(bool disposing) { if (!_isDisposed) { - if (disposing) - { - _serializerState.Dispose(); - } - _isDisposed = true; } } diff --git a/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs b/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs index b9f7aea0..c02873c8 100644 --- a/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs +++ b/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs @@ -52,7 +52,6 @@ private static void WriteTextWriter(TextWriter writer, in TValue? value, using var context = new PaYamlSerializationContext(options); var builder = new SerializerBuilder(); context.ApplyToSerializerBuilder(builder); - context.ValueSerializer = builder.BuildValueSerializer(); var serializer = builder.Build(); try @@ -104,16 +103,12 @@ private static void WriteTextWriter(TextWriter writer, in TValue? value, using var context = new PaYamlSerializationContext(options); var builder = new DeserializerBuilder(); context.ApplyToDeserializerBuilder(builder); - context.ValueDeserializer = builder.BuildValueDeserializer(); var serializer = builder.Build(); try { var value = serializer.Deserialize(reader); - // Must call OnDeserialization to invoke any post-deserialization callbacks on the deserialized object tree. - context.OnDeserialization(); - return value; } catch (YamlException ex) diff --git a/src/Persistence/PaYaml/Serialization/YamlConverterOfT.cs b/src/Persistence/PaYaml/Serialization/YamlConverterOfT.cs index 142ce941..a933c7bc 100644 --- a/src/Persistence/PaYaml/Serialization/YamlConverterOfT.cs +++ b/src/Persistence/PaYaml/Serialization/YamlConverterOfT.cs @@ -9,15 +9,8 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; public abstract class YamlConverter : IYamlTypeConverter { - protected YamlConverter(PaYamlSerializationContext context) - { - SerializationContext = context ?? throw new ArgumentNullException(nameof(context)); - } - public Type Type { get; } = typeof(T); - public PaYamlSerializationContext SerializationContext { get; } - /// /// The default implementation returns true when equals typeof(). /// @@ -26,19 +19,19 @@ public virtual bool Accepts(Type type) return type == Type; } - public abstract T ReadYaml(IParser parser, Type typeToConvert); + public abstract T ReadYaml(IParser parser, Type typeToConvert, ObjectDeserializer rootDeserializer); - public abstract void WriteYaml(IEmitter emitter, T? value, Type typeToConvert); + public abstract void WriteYaml(IEmitter emitter, T? value, Type typeToConvert, ObjectSerializer serializer); - object? IYamlTypeConverter.ReadYaml(IParser parser, Type typeToConvert) + object? IYamlTypeConverter.ReadYaml(IParser parser, Type typeToConvert, ObjectDeserializer rootDeserializer) { - return ReadYaml(parser, typeToConvert); + return ReadYaml(parser, typeToConvert, rootDeserializer); } - void IYamlTypeConverter.WriteYaml(IEmitter emitter, object? value, Type typeToConvert) + void IYamlTypeConverter.WriteYaml(IEmitter emitter, object? value, Type typeToConvert, ObjectSerializer serializer) { var valueOfT = YamlSerialization.UnboxOnWrite(value); - WriteYaml(emitter, valueOfT, typeToConvert); + WriteYaml(emitter, valueOfT, typeToConvert, serializer); } } diff --git a/src/Persistence/PaYaml/Serialization/YamlDotNetExtensions.cs b/src/Persistence/PaYaml/Serialization/YamlDotNetExtensions.cs index 27d9ab0c..5c544ff7 100644 --- a/src/Persistence/PaYaml/Serialization/YamlDotNetExtensions.cs +++ b/src/Persistence/PaYaml/Serialization/YamlDotNetExtensions.cs @@ -23,7 +23,7 @@ public static bool TryConsumeNull(this IParser parser) // NullNodeDeserializer.Deserialize is undocumented, but here's a good summary of what it does: // Attempts to consume the current node event iif it represents a YAML null value. Otherwise, the current event stays. // Returns true if the current node was a null value and was consumed; otherwise, false. - return _nullNodeDeserializer.Deserialize(parser, _typeofObject, null!, out _); + return _nullNodeDeserializer.Deserialize(parser, _typeofObject, null!, out _, null!); } public static void EmitNull(this IEmitter emitter) diff --git a/src/Persistence/PersistenceErrorCode.cs b/src/Persistence/PersistenceErrorCode.cs index 84984c7f..61220186 100644 --- a/src/Persistence/PersistenceErrorCode.cs +++ b/src/Persistence/PersistenceErrorCode.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; using System.ComponentModel; namespace Microsoft.PowerPlatform.PowerApps.Persistence; diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index 98a61da4..1b6927cf 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -82,9 +82,9 @@ }, "YamlDotNet": { "type": "Direct", - "requested": "[15.1.6, )", - "resolved": "15.1.6", - "contentHash": "T/cQEK/KHK96Q8kytJ4iUGDXg1/fj2Qtk6rCQeIlHYU1zTeyGVHW0QNZgREQyxZpygGMDMmrXNWt0sj5TsQnjA==" + "requested": "[16.1.3, )", + "resolved": "16.1.3", + "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", @@ -144,9 +144,9 @@ }, "YamlDotNet": { "type": "Direct", - "requested": "[15.1.6, )", - "resolved": "15.1.6", - "contentHash": "T/cQEK/KHK96Q8kytJ4iUGDXg1/fj2Qtk6rCQeIlHYU1zTeyGVHW0QNZgREQyxZpygGMDMmrXNWt0sj5TsQnjA==" + "requested": "[16.1.3, )", + "resolved": "16.1.3", + "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" } }, "net8.0": { @@ -167,9 +167,9 @@ }, "YamlDotNet": { "type": "Direct", - "requested": "[15.1.6, )", - "resolved": "15.1.6", - "contentHash": "T/cQEK/KHK96Q8kytJ4iUGDXg1/fj2Qtk6rCQeIlHYU1zTeyGVHW0QNZgREQyxZpygGMDMmrXNWt0sj5TsQnjA==" + "requested": "[16.1.3, )", + "resolved": "16.1.3", + "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" } } }