From 5a97c598b1db61eed260616f1bcbcd1857b4ea1e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 27 Aug 2026 05:34:35 -0700 Subject: [PATCH] Add coverage for marshalling a dictionary's Keys/Values collections Binding a XAML control to 'someDictionary.Values' (issue #2537) marshals the 'Dictionary.KeyCollection'/'ValueCollection' nested types across the ABI, not the dictionary itself, so each needs a CCW exposing 'IIterable' and 'IBindableIterable'. Neither type is ever named in user code: 'Keys'/'Values' are declared as 'IEnumerable' on 'IReadOnlyDictionary' and as 'ICollection' on 'IDictionary'. The interop generator already handles this, off the dictionary instantiation itself: for every type specification it sees, it resolves the definition and instantiates the signatures of its members with that generic context, so 'Dictionary' yields the constructed nested collection types returned by 'get_Keys'/'get_Values'. Verified by adding a dictionary instantiation over an otherwise unused pair of type arguments and observing the matching CCW entries appear in the generated 'WinRT.Interop.dll'. That was untested, though, so this pins it down: the test identifies the two types by name rather than naming them in code (which would put them in the test assembly's metadata and make the coverage vacuous), checks the CCW answers 'QueryInterface' for both interfaces, and then enumerates the keys from native code through both 'IIterable' and the bindable iterable that XAML uses for an 'ItemsSource'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../UnitTest/TestComponentCSharp_Tests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 06061bee5..286293ddc 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -2804,6 +2804,87 @@ static void AssertHasInterface(void* ccw, in Guid iid) } } + [TestMethod] + public unsafe void TestDictionaryKeyAndValueCollectionInterfaceMarshalling() + { + // Binding a XAML control to a dictionary's keys or values (e.g. '{x:Bind vm.Dict.Values}') marshals + // the 'Dictionary.KeyCollection'/'ValueCollection' nested types across the ABI, not the + // dictionary itself, which builds a CCW for them. Those types are never named in user code, because + // 'Keys'/'Values' are declared as 'IEnumerable' on 'IReadOnlyDictionary' and as + // 'ICollection' on 'IDictionary', so they only get one if the interop generator + // tracks them off the dictionary instantiation itself. + Dictionary dictionary = new() { [0] = "zero", [1] = "one", [2] = "two" }; + + IReadOnlyDictionary readOnlyDictionary = dictionary; + IDictionary mutableDictionary = dictionary; + + IEnumerable readOnlyKeys = readOnlyDictionary.Keys; + IEnumerable readOnlyValues = readOnlyDictionary.Values; + ICollection keys = mutableDictionary.Keys; + ICollection values = mutableDictionary.Values; + + // Guard against the BCL changing which types it hands out, so that this test fails loudly rather + // than silently covering nothing. The types are deliberately identified by name: naming them in + // code would put them in this assembly's metadata, which is precisely what the scenario cannot + // rely on (the whole point is that they are only reachable through the interface members). + AssertIsDictionaryCollection(readOnlyKeys, "KeyCollection"); + AssertIsDictionaryCollection(readOnlyValues, "ValueCollection"); + AssertIsDictionaryCollection(keys, "KeyCollection"); + AssertIsDictionaryCollection(values, "ValueCollection"); + + // 'IEnumerable' is projected as 'IIterable', and 'IEnumerable' as 'IBindableIterable' + AssertCcwExposesIterableInterfaces(readOnlyKeys, new Guid("81A643FB-F51C-5565-83C4-F96425777B66")); + AssertCcwExposesIterableInterfaces(readOnlyValues, new Guid("E2FCC7C1-3BFC-5A0B-B2B0-72E769D1CB7E")); + + // Enumerate the keys from native code, through 'IIterable' + int sum = 0; + + using (IEnumerator iterator = TestObject.GetIteratorForCollection(readOnlyKeys)) + { + while (iterator.MoveNext()) + { + sum += iterator.Current; + } + } + + Assert.AreEqual(3, sum); + + // Same thing through a projected API taking a bindable iterable, which is what XAML uses to bind + // an 'ItemsSource'. The values have to be sequential from 0, because the native setter validates that. + TestObject.BindableIterableProperty = readOnlyKeys; + CollectionAssert.AreEqual(new[] { 0, 1, 2 }, TestObject.BindableIterableProperty.Cast().ToArray()); + + static void AssertIsDictionaryCollection(IEnumerable source, string name) + { + Assert.AreEqual($"System.Collections.Generic.Dictionary`2+{name}", source.GetType().GetGenericTypeDefinition().FullName); + } + + static void AssertCcwExposesIterableInterfaces(IEnumerable source, Guid iidIIterable) + { + Guid iidIBindableIterable = new("036D2C08-DF29-41AF-8AA2-D774BE62BA6F"); + + void* ccw = WindowsRuntimeMarshal.ConvertToUnmanaged(source); + + try + { + AssertHasInterface(ccw, in iidIIterable); + AssertHasInterface(ccw, in iidIBindableIterable); + } + finally + { + _ = Marshal.Release((nint)ccw); + } + + static void AssertHasInterface(void* ccw, in Guid iid) + { + Marshal.ThrowExceptionForHR(Marshal.QueryInterface((nint)ccw, in iid, out nint interfaceCcw)); + Assert.AreNotEqual(IntPtr.Zero, interfaceCcw); + + _ = Marshal.Release(interfaceCcw); + } + } + } + [TestMethod] public void TestClassGeneric() {