Skip to content

Commit ac7dd0f

Browse files
committed
Don't send yield-return iterators across AppDomain boundaries
To access the metadata of items from another AppDomain we can't use yield return because the generated iterator class is not marked as [Serializable]. Provide eager codepaths for cross-AppDomain scenarios that return simple arrays. We can't always return simple arrays because in the general case we want to stream metadata directly to consumers without allocating a collection to store the results.
1 parent db022c0 commit ac7dd0f

File tree

3 files changed

+110
-8
lines changed

3 files changed

+110
-8
lines changed

src/Build/Instance/ProjectItemInstance.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,16 @@ public IEnumerable<KeyValuePair<string, string>> EnumerateMetadata()
10681068
var list = _itemDefinitions != null ? MetadataCollection : _directMetadata;
10691069
if (list != null)
10701070
{
1071+
#if FEATURE_APPDOMAIN
1072+
// Can't send a yield-return iterator across AppDomain boundaries
1073+
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
1074+
{
1075+
return EnumerateMetadataEager(list);
1076+
}
1077+
#endif
1078+
// Mainline scenario, returns an iterator to avoid allocating an array
1079+
// to store the results. With the iterator, results can stream to the
1080+
// consumer (e.g. binlog writer) without allocations.
10711081
return EnumerateMetadata(list);
10721082
}
10731083
else
@@ -1076,6 +1086,28 @@ public IEnumerable<KeyValuePair<string, string>> EnumerateMetadata()
10761086
}
10771087
}
10781088

1089+
/// <summary>
1090+
/// Used to return metadata from another AppDomain. Can't use yield return because the
1091+
/// generated state machine is not marked as [Serializable], so we need to allocate.
1092+
/// </summary>
1093+
/// <param name="list">The source list to return metadata from.</param>
1094+
/// <returns>An array of string key-value pairs representing metadata.</returns>
1095+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager(CopyOnWritePropertyDictionary<ProjectMetadataInstance> list)
1096+
{
1097+
var result = new List<KeyValuePair<string, string>>(list.Count);
1098+
1099+
foreach (var projectMetadataInstance in list)
1100+
{
1101+
if (projectMetadataInstance != null)
1102+
{
1103+
result.Add(new KeyValuePair<string, string>(projectMetadataInstance.Name, projectMetadataInstance.EvaluatedValue));
1104+
}
1105+
}
1106+
1107+
// Probably better to send the raw array across the wire even if it's another allocation.
1108+
return result.ToArray();
1109+
}
1110+
10791111
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadata(CopyOnWritePropertyDictionary<ProjectMetadataInstance> list)
10801112
{
10811113
foreach (var projectMetadataInstance in list)
@@ -1152,7 +1184,7 @@ internal CopyOnWritePropertyDictionary<ProjectMetadataInstance> MetadataCollecti
11521184

11531185
IEnumerable<ProjectMetadataInstance> IItem<ProjectMetadataInstance>.Metadata => MetadataCollection;
11541186

1155-
#region Operators
1187+
#region Operators
11561188

11571189
/// <summary>
11581190
/// This allows an explicit typecast from a "TaskItem" to a "string", returning the ItemSpec for this item.
@@ -1193,7 +1225,7 @@ public static explicit operator string(TaskItem that)
11931225
return !(left == right);
11941226
}
11951227

1196-
#endregion
1228+
#endregion
11971229

11981230
/// <summary>
11991231
/// Produce a string representation.
@@ -1215,7 +1247,7 @@ public override object InitializeLifetimeService()
12151247
}
12161248
#endif
12171249

1218-
#region IItem and ITaskItem2 Members
1250+
#region IItem and ITaskItem2 Members
12191251

12201252
/// <summary>
12211253
/// Returns the metadata with the specified key.
@@ -1467,9 +1499,9 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped()
14671499
return clonedMetadata;
14681500
}
14691501

1470-
#endregion
1502+
#endregion
14711503

1472-
#region INodePacketTranslatable Members
1504+
#region INodePacketTranslatable Members
14731505

14741506
/// <summary>
14751507
/// Reads or writes the packet to the serializer.
@@ -1499,9 +1531,9 @@ void ITranslatable.Translate(ITranslator translator)
14991531
}
15001532
}
15011533

1502-
#endregion
1534+
#endregion
15031535

1504-
#region IEquatable<TaskItem> Members
1536+
#region IEquatable<TaskItem> Members
15051537

15061538
/// <summary>
15071539
/// Override of GetHashCode.
@@ -1579,7 +1611,7 @@ public bool Equals(TaskItem other)
15791611
return true;
15801612
}
15811613

1582-
#endregion
1614+
#endregion
15831615

15841616
/// <summary>
15851617
/// Returns true if a particular piece of metadata is defined on this item (even if

src/Shared/TaskParameter.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,41 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped()
759759

760760
public IEnumerable<KeyValuePair<string, string>> EnumerateMetadata()
761761
{
762+
#if FEATURE_APPDOMAIN
763+
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
764+
{
765+
return EnumerateMetadataEager();
766+
}
767+
#endif
768+
769+
return EnumerateMetadataLazy();
770+
}
771+
772+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager()
773+
{
774+
if (_customEscapedMetadata == null || _customEscapedMetadata.Count == 0)
775+
{
776+
#if TASKHOST
777+
// MSBuildTaskHost.dll compiles against .NET 3.5 which doesn't have Array.Empty()
778+
return new KeyValuePair<string, string>[0];
779+
#else
780+
return Array.Empty<KeyValuePair<string, string>>();
781+
#endif
782+
}
783+
784+
var result = new KeyValuePair<string, string>[_customEscapedMetadata.Count];
785+
int index = 0;
786+
foreach (var kvp in _customEscapedMetadata)
787+
{
788+
var unescaped = new KeyValuePair<string, string>(kvp.Key, EscapingUtilities.UnescapeAll(kvp.Value));
789+
result[index++] = unescaped;
790+
}
791+
792+
return result;
793+
}
794+
795+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataLazy()
796+
{
762797
if (_customEscapedMetadata == null)
763798
{
764799
yield break;

src/Utilities/TaskItem.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,41 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped() => _metadata == null
465465

466466
IEnumerable<KeyValuePair<string, string>> IMetadataContainer.EnumerateMetadata()
467467
{
468+
#if FEATURE_APPDOMAIN
469+
// Can't send a yield-return iterator across AppDomain boundaries
470+
// so have to allocate
471+
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
472+
{
473+
return EnumerateMetadataEager();
474+
}
475+
#endif
476+
477+
// In general case we want to return an iterator without allocating a collection
478+
// to hold the result, so we can stream the items directly to the consumer.
479+
return EnumerateMetadataLazy();
480+
}
481+
482+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager()
483+
{
484+
if (_metadata == null)
485+
{
486+
return Array.Empty<KeyValuePair<string, string>>();
487+
}
488+
489+
int count = _metadata.Count;
490+
int index = 0;
491+
var result = new KeyValuePair<string, string>[count];
492+
foreach (var kvp in _metadata)
493+
{
494+
var unescaped = new KeyValuePair<string, string>(kvp.Key, EscapingUtilities.UnescapeAll(kvp.Value));
495+
result[index++] = unescaped;
496+
}
497+
498+
return result;
499+
}
500+
501+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataLazy()
502+
{
468503
if (_metadata == null)
469504
{
470505
yield break;

0 commit comments

Comments
 (0)