diff --git a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
index 7ca94153d09..f4644d4e358 100644
--- a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
@@ -1029,6 +1029,25 @@ public void TestTaskResolutionFailureWithNoUsingTask()
_logger.AssertLogContains("MSB4036");
}
+ ///
+ /// https://github.com/dotnet/msbuild/issues/8864
+ ///
+ [Fact]
+ public void TestTaskDictionaryOutputItems()
+ {
+ string customTaskPath = Assembly.GetExecutingAssembly().Location;
+ MockLogger ml = ObjectModelHelpers.BuildProjectExpectSuccess($"""
+
+
+
+
+
+
+
+
+ """);
+ ml.AssertLogContains("a=b");
+ }
#endregion
#region ITestTaskHost Members
@@ -1423,11 +1442,11 @@ private ProjectInstance CreateTestProject()
-
-
-
-
-
+
+
+
+
+
diff --git a/src/Build.UnitTests/BackEnd/TaskThatReturnsDictionaryTaskItem.cs b/src/Build.UnitTests/BackEnd/TaskThatReturnsDictionaryTaskItem.cs
new file mode 100644
index 00000000000..c258beb89a4
--- /dev/null
+++ b/src/Build.UnitTests/BackEnd/TaskThatReturnsDictionaryTaskItem.cs
@@ -0,0 +1,210 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Build.Framework;
+
+#nullable disable
+
+namespace Microsoft.Build.Engine.UnitTests;
+///
+/// Task that returns a custom ITaskItem implementation that has a custom IDictionary type returned from CloneCustomMetadata()
+///
+public sealed class TaskThatReturnsDictionaryTaskItem : Utilities.Task
+{
+ public string Key { get; set; }
+ public string Value { get; set; }
+
+ public override bool Execute()
+ {
+ var metaValue = new MinimalDictionary
+ {
+ { Key, Value }
+ };
+ DictionaryTaskItemOutput = new MinimalDictionaryTaskItem(metaValue);
+ return true;
+ }
+
+ [Output]
+ public ITaskItem DictionaryTaskItemOutput { get; set; }
+
+ internal sealed class MinimalDictionaryTaskItem : ITaskItem
+ {
+ private MinimalDictionary _metaData = new MinimalDictionary();
+
+ public MinimalDictionaryTaskItem(MinimalDictionary metaValue)
+ {
+ _metaData = metaValue;
+ }
+
+ public string ItemSpec { get => $"{nameof(MinimalDictionaryTaskItem)}spec"; set => throw new NotImplementedException(); }
+
+ public ICollection MetadataNames => throw new NotImplementedException();
+
+ public int MetadataCount => throw new NotImplementedException();
+
+ ICollection ITaskItem.MetadataNames => throw new NotImplementedException();
+
+ public IDictionary CloneCustomMetadata() => _metaData;
+
+ public string GetMetadata(string metadataName)
+ {
+ if (String.IsNullOrEmpty(metadataName))
+ {
+ throw new ArgumentNullException(nameof(metadataName));
+ }
+
+ string value = (string)_metaData[metadataName];
+ return value;
+ }
+
+ public void SetMetadata(string metadataName, string metadataValue) => throw new NotImplementedException();
+ public void RemoveMetadata(string metadataName) => throw new NotImplementedException();
+ public void CopyMetadataTo(ITaskItem destinationItem) => throw new NotImplementedException();
+ }
+}
+
+public sealed class MinimalDictionary : IDictionary
+{
+ private List _keys = new List();
+ private List _values = new List();
+
+ public object this[object key]
+ {
+ get
+ {
+ int index = _keys.IndexOf((TKey)key);
+ return index == -1 ? throw new KeyNotFoundException() : (object)_values[index];
+ }
+ set
+ {
+ int index = _keys.IndexOf((TKey)key);
+ if (index == -1)
+ {
+ _keys.Add((TKey)key);
+ _values.Add((TValue)value);
+ }
+ else
+ {
+ _values[index] = (TValue)value;
+ }
+ }
+ }
+
+ public bool IsFixedSize => false;
+
+ public bool IsReadOnly => false;
+
+ public ICollection Keys => _keys;
+
+ public ICollection Values => _values;
+
+ public int Count => _keys.Count;
+
+ public bool IsSynchronized => false;
+
+ public object SyncRoot => throw new NotSupportedException();
+
+ public void Add(object key, object value)
+ {
+ if (_keys.Contains((TKey)key))
+ {
+ throw new ArgumentException("An item with the same key has already been added.");
+ }
+
+ _keys.Add((TKey)key);
+ _values.Add((TValue)value);
+ }
+
+ public void Clear()
+ {
+ _keys.Clear();
+ _values.Clear();
+ }
+
+ public bool Contains(object key)
+ {
+ return _keys.Contains((TKey)key);
+ }
+
+ public void CopyTo(Array array, int index)
+ {
+ if (array == null)
+ {
+ throw new ArgumentNullException(nameof(array));
+ }
+
+ if (array.Rank != 1)
+ {
+ throw new ArgumentException("Array must be one-dimensional.", nameof(array));
+ }
+
+ if (index < 0 || index > array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ if (array.Length - index < Count)
+ {
+ throw new ArgumentException("The number of elements in the source is greater than the available space from index to the end of the destination array.");
+ }
+
+ for (int i = 0; i < Count; i++)
+ {
+ array.SetValue(new KeyValuePair(_keys[i], _values[i]), index + i);
+ }
+ }
+
+ public IDictionaryEnumerator GetEnumerator() => new MinimalDictionaryEnumerator(_keys, _values);
+
+ public void Remove(object key)
+ {
+ int index = _keys.IndexOf((TKey)key);
+ if (index != -1)
+ {
+ _keys.RemoveAt(index);
+ _values.RemoveAt(index);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ yield return new KeyValuePair(_keys[i], _values[i]);
+ }
+ }
+
+ private sealed class MinimalDictionaryEnumerator : IDictionaryEnumerator
+ {
+ private List _keys;
+ private List _values;
+ private int _index = -1;
+
+ public MinimalDictionaryEnumerator(List keys, List values)
+ {
+ _keys = keys;
+ _values = values;
+ }
+
+ public object Current => Entry;
+
+ public object Key => _keys[_index];
+
+ public object Value => _values[_index];
+
+ public DictionaryEntry Entry => new DictionaryEntry(Key, Value);
+
+ public bool MoveNext()
+ {
+ return ++_index < _keys.Count;
+ }
+
+ public void Reset()
+ {
+ _index = -1;
+ }
+ }
+}