From b198c5639695c97a9e18b8fab5f35ad0a4ce22dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:46:50 +0000 Subject: [PATCH 1/4] Initial plan From f247d727445c26553a419d64cb17e6a8994d0c6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:52:17 +0000 Subject: [PATCH 2/4] Fix GetPropertyValue to support nested partition key paths Co-authored-by: philnach <19275540+philnach@users.noreply.github.com> --- .../CosmosDataSinkExtensionTests.cs | 118 ++++++++++++++++++ .../CosmosDataSinkExtension.cs | 25 +++- 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs index fdb5bf6..ca2961a 100644 --- a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs +++ b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs @@ -366,5 +366,123 @@ public static bool HasProperty(object obj, string name) var values = (IDictionary)obj; return values.ContainsKey(name); } + + [TestMethod] + public void GetPropertyValue_WithSimpleProperty_ReturnsValue() + { + // Arrange + var expando = new ExpandoObject(); + var dict = (IDictionary)expando; + dict["id"] = "test-id-123"; + dict["name"] = "test-name"; + + // Act - Use reflection to call the private GetPropertyValue method + var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var result = method?.Invoke(null, new object[] { expando, "id" }); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("test-id-123", result); + } + + [TestMethod] + public void GetPropertyValue_WithNestedProperty_ReturnsValue() + { + // Arrange - Create nested structure matching the issue example + var expando = new ExpandoObject(); + var dict = (IDictionary)expando; + dict["id"] = "test-id"; + + var nestedExpando = new ExpandoObject(); + var nestedDict = (IDictionary)nestedExpando; + nestedDict["partitionkeyvalue2"] = "guid-value-123"; + nestedDict["somevalue4"] = "other-guid"; + nestedDict["UserName"] = "testuser"; + + dict["partitionkeyvalue1"] = nestedExpando; + + // Act - Use reflection to call the private GetPropertyValue method + var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var result = method?.Invoke(null, new object[] { expando, "partitionkeyvalue1/partitionkeyvalue2" }); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("guid-value-123", result); + } + + [TestMethod] + public void GetPropertyValue_WithDeeplyNestedProperty_ReturnsValue() + { + // Arrange - Create deeply nested structure + var expando = new ExpandoObject(); + var dict = (IDictionary)expando; + dict["id"] = "test-id"; + + var level1 = new ExpandoObject(); + var level1Dict = (IDictionary)level1; + + var level2 = new ExpandoObject(); + var level2Dict = (IDictionary)level2; + + var level3 = new ExpandoObject(); + var level3Dict = (IDictionary)level3; + level3Dict["finalValue"] = "deeply-nested-value"; + + level2Dict["level3"] = level3; + level1Dict["level2"] = level2; + dict["level1"] = level1; + + // Act + var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var result = method?.Invoke(null, new object[] { expando, "level1/level2/level3/finalValue" }); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("deeply-nested-value", result); + } + + [TestMethod] + public void GetPropertyValue_WithMissingNestedProperty_ReturnsNull() + { + // Arrange + var expando = new ExpandoObject(); + var dict = (IDictionary)expando; + dict["id"] = "test-id"; + + var nestedExpando = new ExpandoObject(); + var nestedDict = (IDictionary)nestedExpando; + nestedDict["existingKey"] = "value"; + + dict["parent"] = nestedExpando; + + // Act + var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var result = method?.Invoke(null, new object[] { expando, "parent/nonExistentKey" }); + + // Assert + Assert.IsNull(result); + } + + [TestMethod] + public void GetPropertyValue_WithNullIntermediateValue_ReturnsNull() + { + // Arrange + var expando = new ExpandoObject(); + var dict = (IDictionary)expando; + dict["id"] = "test-id"; + dict["parent"] = null; + + // Act + var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var result = method?.Invoke(null, new object[] { expando, "parent/child" }); + + // Assert + Assert.IsNull(result); + } } } diff --git a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs index 3305391..b82c186 100644 --- a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs +++ b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs @@ -286,7 +286,30 @@ private static MemoryStream CreateItemStream(ExpandoObject item) private static string? GetPropertyValue(ExpandoObject item, string propertyName) { - return ((IDictionary)item)[propertyName]?.ToString(); + // Handle nested property paths (e.g., "property1/property2/property3") + var pathSegments = propertyName.Split('/'); + object? current = item; + + foreach (var segment in pathSegments) + { + if (current == null) + { + return null; + } + + if (current is not ExpandoObject expandoObj) + { + return null; + } + + var dict = (IDictionary)expandoObj; + if (!dict.TryGetValue(segment, out current)) + { + return null; + } + } + + return current?.ToString(); } public IEnumerable GetSettings() From aa5600439282fcd7834dcd4b6d6a362a33f14c5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:54:57 +0000 Subject: [PATCH 3/4] Address code review feedback: handle leading slashes and reduce code duplication Co-authored-by: philnach <19275540+philnach@users.noreply.github.com> --- .../CosmosDataSinkExtensionTests.cs | 53 +++++++++++++------ .../CosmosDataSinkExtension.cs | 3 +- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs index ca2961a..0e574a4 100644 --- a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs +++ b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs @@ -367,6 +367,13 @@ public static bool HasProperty(object obj, string name) return values.ContainsKey(name); } + private static string? InvokeGetPropertyValue(ExpandoObject item, string propertyName) + { + var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + return method?.Invoke(null, new object[] { item, propertyName }) as string; + } + [TestMethod] public void GetPropertyValue_WithSimpleProperty_ReturnsValue() { @@ -376,10 +383,8 @@ public void GetPropertyValue_WithSimpleProperty_ReturnsValue() dict["id"] = "test-id-123"; dict["name"] = "test-name"; - // Act - Use reflection to call the private GetPropertyValue method - var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - var result = method?.Invoke(null, new object[] { expando, "id" }); + // Act + var result = InvokeGetPropertyValue(expando, "id"); // Assert Assert.IsNotNull(result); @@ -402,10 +407,8 @@ public void GetPropertyValue_WithNestedProperty_ReturnsValue() dict["partitionkeyvalue1"] = nestedExpando; - // Act - Use reflection to call the private GetPropertyValue method - var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - var result = method?.Invoke(null, new object[] { expando, "partitionkeyvalue1/partitionkeyvalue2" }); + // Act + var result = InvokeGetPropertyValue(expando, "partitionkeyvalue1/partitionkeyvalue2"); // Assert Assert.IsNotNull(result); @@ -435,9 +438,7 @@ public void GetPropertyValue_WithDeeplyNestedProperty_ReturnsValue() dict["level1"] = level1; // Act - var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - var result = method?.Invoke(null, new object[] { expando, "level1/level2/level3/finalValue" }); + var result = InvokeGetPropertyValue(expando, "level1/level2/level3/finalValue"); // Assert Assert.IsNotNull(result); @@ -459,9 +460,7 @@ public void GetPropertyValue_WithMissingNestedProperty_ReturnsNull() dict["parent"] = nestedExpando; // Act - var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - var result = method?.Invoke(null, new object[] { expando, "parent/nonExistentKey" }); + var result = InvokeGetPropertyValue(expando, "parent/nonExistentKey"); // Assert Assert.IsNull(result); @@ -477,12 +476,32 @@ public void GetPropertyValue_WithNullIntermediateValue_ReturnsNull() dict["parent"] = null; // Act - var method = typeof(CosmosDataSinkExtension).GetMethod("GetPropertyValue", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - var result = method?.Invoke(null, new object[] { expando, "parent/child" }); + var result = InvokeGetPropertyValue(expando, "parent/child"); // Assert Assert.IsNull(result); } + + [TestMethod] + public void GetPropertyValue_WithLeadingSlash_WorksCorrectly() + { + // Arrange - Test that paths with leading slash work (defensive programming) + var expando = new ExpandoObject(); + var dict = (IDictionary)expando; + dict["id"] = "test-id"; + + var nestedExpando = new ExpandoObject(); + var nestedDict = (IDictionary)nestedExpando; + nestedDict["partitionkey"] = "pk-value"; + + dict["parent"] = nestedExpando; + + // Act - Path with leading slash (though TrimStart is called in actual code) + var result = InvokeGetPropertyValue(expando, "/parent/partitionkey"); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("pk-value", result); + } } } diff --git a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs index b82c186..b77a30f 100644 --- a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs +++ b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs @@ -287,7 +287,8 @@ private static MemoryStream CreateItemStream(ExpandoObject item) private static string? GetPropertyValue(ExpandoObject item, string propertyName) { // Handle nested property paths (e.g., "property1/property2/property3") - var pathSegments = propertyName.Split('/'); + // Split and remove empty entries to handle paths starting with '/' (e.g., "/property/nested") + var pathSegments = propertyName.Split('/', StringSplitOptions.RemoveEmptyEntries); object? current = item; foreach (var segment in pathSegments) From e3bb40ff9f77693d65f5ae0319d4ff465e0969e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:57:11 +0000 Subject: [PATCH 4/4] Remove redundant handling of leading slashes Co-authored-by: philnach <19275540+philnach@users.noreply.github.com> --- .../CosmosDataSinkExtensionTests.cs | 22 ------------------- .../CosmosDataSinkExtension.cs | 4 ++-- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs index 0e574a4..690a06b 100644 --- a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs +++ b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/CosmosDataSinkExtensionTests.cs @@ -481,27 +481,5 @@ public void GetPropertyValue_WithNullIntermediateValue_ReturnsNull() // Assert Assert.IsNull(result); } - - [TestMethod] - public void GetPropertyValue_WithLeadingSlash_WorksCorrectly() - { - // Arrange - Test that paths with leading slash work (defensive programming) - var expando = new ExpandoObject(); - var dict = (IDictionary)expando; - dict["id"] = "test-id"; - - var nestedExpando = new ExpandoObject(); - var nestedDict = (IDictionary)nestedExpando; - nestedDict["partitionkey"] = "pk-value"; - - dict["parent"] = nestedExpando; - - // Act - Path with leading slash (though TrimStart is called in actual code) - var result = InvokeGetPropertyValue(expando, "/parent/partitionkey"); - - // Assert - Assert.IsNotNull(result); - Assert.AreEqual("pk-value", result); - } } } diff --git a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs index b77a30f..f240f5d 100644 --- a/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs +++ b/Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension/CosmosDataSinkExtension.cs @@ -287,8 +287,8 @@ private static MemoryStream CreateItemStream(ExpandoObject item) private static string? GetPropertyValue(ExpandoObject item, string propertyName) { // Handle nested property paths (e.g., "property1/property2/property3") - // Split and remove empty entries to handle paths starting with '/' (e.g., "/property/nested") - var pathSegments = propertyName.Split('/', StringSplitOptions.RemoveEmptyEntries); + // Note: Calling code uses TrimStart('/') to remove leading slash before calling this method + var pathSegments = propertyName.Split('/'); object? current = item; foreach (var segment in pathSegments)