diff --git a/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs b/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs
index 7748a189690..1e6b56edd0e 100644
--- a/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs
@@ -596,6 +596,31 @@ public void NullMetadataOnLegacyOutputItems()
logger.AssertLogContains("[foo: ]");
}
+ ///
+ /// If an item returned from a task has bare-minimum metadata implementation, we shouldn't crash.
+ ///
+ [Fact]
+ public void MinimalLegacyOutputItems()
+ {
+ string customTaskPath = Assembly.GetExecutingAssembly().Location;
+
+ string projectContents = $"""
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents, _testOutput, LoggerVerbosity.Diagnostic);
+ }
+
///
/// Regression test for https://github.com/dotnet/msbuild/issues/5080
///
diff --git a/src/Build.UnitTests/TaskThatReturnsMinimalItem.cs b/src/Build.UnitTests/TaskThatReturnsMinimalItem.cs
new file mode 100644
index 00000000000..7f8eec32b2a
--- /dev/null
+++ b/src/Build.UnitTests/TaskThatReturnsMinimalItem.cs
@@ -0,0 +1,48 @@
+// 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 Microsoft.Build.Framework;
+
+namespace Microsoft.Build.Engine.UnitTests;
+
+///
+/// Task that emulates .NET 3.5 tasks.
+///
+public sealed class TaskThatReturnsMinimalItem : ITask
+{
+ public IBuildEngine? BuildEngine { get; set; }
+ public ITaskHost? HostObject { get; set; }
+
+ [Output]
+ public ITaskItem MinimalTaskItemOutput { get => new MinimalTaskItem(); }
+
+ public bool Execute() => true;
+
+ ///
+ /// Minimal implementation of that uses a for metadata,
+ /// like MSBuild 3 did.
+ ///
+ internal sealed class MinimalTaskItem : ITaskItem
+ {
+ public string ItemSpec { get => $"{nameof(MinimalTaskItem)}spec"; set => throw new NotImplementedException(); }
+
+ public ICollection MetadataNames => throw new NotImplementedException();
+
+ public int MetadataCount => throw new NotImplementedException();
+
+ public IDictionary CloneCustomMetadata()
+ {
+ Hashtable t = new();
+ t["key"] = "value";
+
+ return t;
+ }
+ public void CopyMetadataTo(ITaskItem destinationItem) => throw new NotImplementedException();
+ public string GetMetadata(string metadataName) => "value";
+ public void RemoveMetadata(string metadataName) => throw new NotImplementedException();
+ public void SetMetadata(string metadataName, string metadataValue) => throw new NotImplementedException();
+ }
+}
diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs
index 69da00e3955..705ca12979e 100644
--- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs
@@ -1396,8 +1396,8 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN
newItem = new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output.ItemSpec), parameterLocationEscaped);
newItem.SetMetadataOnTaskOutput(output.CloneCustomMetadata()
- .Cast>()
- .Select(x => new KeyValuePair(x.Key, EscapingUtilities.Escape(x.Value))));
+ .Cast()
+ .Select(x => new KeyValuePair((string)x.Key, EscapingUtilities.Escape((string)x.Value))));
}
}
diff --git a/src/Framework/ITaskItemExtensions.cs b/src/Framework/ITaskItemExtensions.cs
index 7dc7dbdaf86..6ba56e1a880 100644
--- a/src/Framework/ITaskItemExtensions.cs
+++ b/src/Framework/ITaskItemExtensions.cs
@@ -35,7 +35,9 @@ public static IEnumerable> EnumerateMetadata(this I
return enumerableMetadata;
}
- // In theory this should never be reachable.
+ // Fallback for
+ // * ITaskItem implementations from MSBuild 3.5 from the GAC
+ // * Custom ITaskItems that don't use Dictionary
var list = new KeyValuePair[customMetadata.Count];
int i = 0;