diff --git a/nuget.config b/nuget.config
index d2686ace..c24cbe65 100644
--- a/nuget.config
+++ b/nuget.config
@@ -2,7 +2,8 @@
-
+
+
-
+
+
+
+
diff --git a/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs b/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs
index e9695fce..583cbfdc 100644
--- a/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs
+++ b/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs
@@ -12,6 +12,14 @@ namespace Persistence.Tests.PaYaml.Serialization;
[TestClass]
public class PaYamlSerializerTests : VSTestBase
{
+ [TestMethod]
+ public void DeserializeAppInstanceSetsLocationInfo()
+ {
+ PaYamlSerializer.Deserialize("App: {}")!.App!.Start.Should().Be(new(1, 6));
+ PaYamlSerializer.Deserialize("\nApp: {}")!.App!.Start.Should().Be(new(2, 6));
+ PaYamlSerializer.Deserialize("\nApp:\n Properties: {}")!.App!.Start.Should().Be(new(3, 2));
+ }
+
[TestMethod]
public void DeserializeNamedObjectSetsLocationInfo()
{
@@ -27,6 +35,7 @@ public void DeserializeNamedObjectSetsLocationInfo()
paModule.App.Properties.ShouldNotBeNull();
paModule.App.Properties.Should().ContainName("Foo").WhoseNamedObject.Start.Should().Be(new(3, 9));
paModule.App.Properties.Should().ContainName("Bar").WhoseNamedObject.Start.Should().Be(new(4, 9));
+ paModule.App.Start.Should().Be(new(2, 5), "`App.Start` location info should now be set correctly");
}
[TestMethod]
diff --git a/src/Persistence/Extensions/JsonExtensions.cs b/src/Persistence/Extensions/JsonExtensions.cs
index efb3977b..9ac8b396 100644
--- a/src/Persistence/Extensions/JsonExtensions.cs
+++ b/src/Persistence/Extensions/JsonExtensions.cs
@@ -8,10 +8,10 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions;
public static class JsonExtensions
{
///
- /// A fluent way of making a instance immutable.
+ /// A fluent way of calling to make a instance immutable.
/// Especially useful for shared static instances.
///
- public static JsonSerializerOptions ToReadOnly(this JsonSerializerOptions options)
+ public static JsonSerializerOptions MakeReadOnlyFluent(this JsonSerializerOptions options)
{
options.MakeReadOnly(populateMissingResolver: true);
return options;
diff --git a/src/Persistence/MsApp/Serialization/MsappSerialization.cs b/src/Persistence/MsApp/Serialization/MsappSerialization.cs
index 9691acfc..52819a52 100644
--- a/src/Persistence/MsApp/Serialization/MsappSerialization.cs
+++ b/src/Persistence/MsApp/Serialization/MsappSerialization.cs
@@ -38,10 +38,10 @@ public static class MsappSerialization
// But this may have impact on other code which depends on this property.
new JsonDateTimeAssumesUtcConverter(),
},
- }.ToReadOnly();
+ }.MakeReadOnlyFluent();
internal static readonly JsonSerializerOptions DocumentJsonSerializeOptions = new JsonSerializerOptions(DefaultSharedJsonSerializeOptions)
- .ToReadOnly();
+ .MakeReadOnlyFluent();
///
/// This should match the options used in DocumentServer for deserializing msapp json files.
@@ -51,7 +51,7 @@ public static class MsappSerialization
{
// Note: The docsvr doesn't indent the Header.json file.
WriteIndented = false,
- }.ToReadOnly();
+ }.MakeReadOnlyFluent();
///
/// Serialization options used for the 'packed.json' file in the msapp archive.
@@ -76,5 +76,5 @@ public static class MsappSerialization
// In order to ensure forward-compatible deserialization, we ignore unknown members
// Any object model that wants to also survive round-tripping, must use JsonExtensionData to capture those unknown members.
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
- }.ToReadOnly();
+ }.MakeReadOnlyFluent();
}
diff --git a/src/Persistence/MsappPacking/Serialization/MsaprSerialization.cs b/src/Persistence/MsappPacking/Serialization/MsaprSerialization.cs
index 833f6bd9..71601e28 100644
--- a/src/Persistence/MsappPacking/Serialization/MsaprSerialization.cs
+++ b/src/Persistence/MsappPacking/Serialization/MsaprSerialization.cs
@@ -39,5 +39,5 @@ public static class MsaprSerialization
// In order to ensure forward-compatible deserialization, we ignore unknown members
// Any object model that wants to also survive round-tripping, must use JsonExtensionData to capture those unknown members.
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
- }.ToReadOnly();
+ }.MakeReadOnlyFluent();
}
diff --git a/src/Persistence/PaYaml/Models/IMayHavePaYamlLocation.cs b/src/Persistence/PaYaml/Models/IMayHavePaYamlLocation.cs
new file mode 100644
index 00000000..f4470407
--- /dev/null
+++ b/src/Persistence/PaYaml/Models/IMayHavePaYamlLocation.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models;
+
+///
+/// An interface for objects that may have a YAML location.
+/// This is used to keep a consistent API for objects that may have a location in the YAML file.
+///
+public interface IMayHavePaYamlLocation
+{
+ ///
+ /// The location in the YAML file where this object starts.
+ ///
+ PaYamlLocation? Start { get; }
+}
diff --git a/src/Persistence/PaYaml/Models/ISetPaYamlNodeLocation.cs b/src/Persistence/PaYaml/Models/ISetPaYamlNodeLocation.cs
new file mode 100644
index 00000000..58a904f3
--- /dev/null
+++ b/src/Persistence/PaYaml/Models/ISetPaYamlNodeLocation.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models;
+
+internal interface ISetPaYamlNodeLocation : IMayHavePaYamlLocation
+{
+ ///
+ /// Sets the location of the node in the YAML file.
+ ///
+ void SetNodeLocation(PaYamlLocation? start);
+}
diff --git a/src/Persistence/PaYaml/Models/SchemaV3/AppInstance.cs b/src/Persistence/PaYaml/Models/SchemaV3/AppInstance.cs
index a2e0c334..2c983e6a 100644
--- a/src/Persistence/PaYaml/Models/SchemaV3/AppInstance.cs
+++ b/src/Persistence/PaYaml/Models/SchemaV3/AppInstance.cs
@@ -6,12 +6,17 @@
namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3;
-public record AppInstance
+public record AppInstance : IMayHavePaYamlLocation, ISetPaYamlNodeLocation
{
[YamlIgnore]
- public PaYamlLocation? Start { get; init; }
+ public PaYamlLocation? Start { get; private set; }
public NamedObjectMapping? Properties { get; init; }
+ void ISetPaYamlNodeLocation.SetNodeLocation(PaYamlLocation? start)
+ {
+ Start = start;
+ }
+
// WorkItem 27966436: Support saving AppHost instances to top-level property 'App'
}
diff --git a/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs b/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs
index 4a3773c4..c5dd2826 100644
--- a/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs
+++ b/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs
@@ -3,6 +3,7 @@
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
+using YamlDotNet.Serialization.NodeDeserializers;
namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization;
@@ -29,6 +30,7 @@ internal void ApplyToDeserializerBuilder(DeserializerBuilder builder)
builder
.WithDuplicateKeyChecking()
.IgnoreFields()
+ .WithSetNodeLocationNodeDeserializerWrapper()
;
if (Options.MaximumRecursion.HasValue)
diff --git a/src/Persistence/PaYaml/Serialization/SetNodeLocationNodeDeserializerWrapper.cs b/src/Persistence/PaYaml/Serialization/SetNodeLocationNodeDeserializerWrapper.cs
new file mode 100644
index 00000000..89d5a187
--- /dev/null
+++ b/src/Persistence/PaYaml/Serialization/SetNodeLocationNodeDeserializerWrapper.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models;
+using YamlDotNet.Core;
+using YamlDotNet.Serialization;
+
+namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization;
+
+///
+/// A node deserializer that wraps an existing which will set the location information on the value if it implements .
+///
+internal sealed class SetNodeLocationNodeDeserializerWrapper(TNodeDeserializerToWrap wrappedNodeDeserializer) : INodeDeserializer
+ where TNodeDeserializerToWrap : INodeDeserializer
+{
+ public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer)
+ {
+ // Capture the start location before deserializing the node
+ var start = reader.Current?.Start;
+ if (wrappedNodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value, rootDeserializer))
+ {
+ if (start != null && value is ISetPaYamlNodeLocation setNodeLocationValue)
+ {
+ // Only set the location if it hasn't been set yet
+ if (setNodeLocationValue.Start is null)
+ {
+ setNodeLocationValue.SetNodeLocation(PaYamlLocation.FromMark(start.Value));
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+}
+
+internal static class SetNodeLocationNodeDeserializerWrapperExtensions
+{
+ ///
+ /// Wraps the specified with a which will set the location information on the value if it implements .
+ ///
+ /// The type of the to be replaced and wrapped.
+ public static DeserializerBuilder WithSetNodeLocationNodeDeserializerWrapper(this DeserializerBuilder builder)
+ where TNodeDeserializerToWrap : INodeDeserializer
+ {
+ _ = builder ?? throw new ArgumentNullException(nameof(builder));
+
+ return builder.WithNodeDeserializer(
+ baseDeserializer => new SetNodeLocationNodeDeserializerWrapper((TNodeDeserializerToWrap)baseDeserializer),
+ s => s.InsteadOf());
+ }
+}