diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs
index cae9e3cf02..fbe2e97b88 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs
@@ -52,8 +52,8 @@ public static class ReflectionMetadataHook
/// All types directly annotated with [TestClass] in the assembly.
///
/// A map from each test class to its [TestMethod]-annotated
- /// set. The dictionary and arrays are copied defensively; the caller may mutate the inputs
- /// after the call.
+ /// set. Ownership transfers to the adapter: the source generator hands over freshly-built
+ /// collections and must not mutate them after the call (see the remarks on the full overload).
///
///
/// Do not call this method from hand-written code; it is meant to be invoked exclusively from
@@ -120,8 +120,20 @@ public static void Register(
/// assigns it directly.
///
///
+ ///
/// Do not call this method from hand-written code; it is meant to be invoked exclusively from
/// the [ModuleInitializer] emitted by the MSTest source generator.
+ ///
+ ///
+ /// Ownership transfer. The adapter takes ownership of every collection passed in
+ /// (the array, the array, and
+ /// each dictionary with its value arrays) and stores them without cloning. Callers MUST hand
+ /// over freshly-built collections and MUST NOT mutate them after the call returns; the source
+ /// generator (the only intended caller) already emits fresh, throwaway collections that satisfy
+ /// this. This is a contract about ownership and mutation, not caller identity: it trades the
+ /// previous defensive copies for zero-copy startup on the understanding that the inputs are the
+ /// adapter's to keep.
+ ///
///
[EditorBrowsable(EditorBrowsableState.Never)]
public static void Register(
@@ -174,29 +186,20 @@ public static void Register(
throw new ArgumentNullException(nameof(propertySetters));
}
- var typesCopy = (Type[])types.Clone();
+ // Ownership transfer (see the remarks on this method): the source generator hands over
+ // freshly-built, throwaway collections and never mutates them after the call, so we store
+ // them directly instead of cloning. Arrays are kept by reference; dictionaries are reused
+ // as-is when already concrete and only materialized when the caller passed some other
+ // IReadOnlyDictionary implementation.
+ Dictionary testMethodsMap = AsOwnedDictionary(testMethods);
+ Dictionary typeAttributesMap = AsOwnedDictionary(typeAttributes);
+ Dictionary> methodInvokersMap = AsOwnedDictionary(methodInvokers);
+ Dictionary> propertySettersMap = AsOwnedDictionary(propertySetters);
- var testMethodsCopy = new Dictionary(testMethods.Count);
- foreach (KeyValuePair kvp in testMethods)
- {
- testMethodsCopy[kvp.Key] = (MethodInfo[])kvp.Value.Clone();
- }
-
- var typeAttributesCopy = new Dictionary(typeAttributes.Count);
- foreach (KeyValuePair kvp in typeAttributes)
- {
- typeAttributesCopy[kvp.Key] = (Attribute[])kvp.Value.Clone();
- }
-
- object[] assemblyAttributesCopy = (object[])assemblyAttributes.Clone();
-
- var methodInvokersCopy = new Dictionary>(methodInvokers.Count);
- foreach (KeyValuePair> kvp in methodInvokers)
- {
- methodInvokersCopy[kvp.Key] = kvp.Value;
- }
-
- var constructorInvokersCopy = new Dictionary(constructorInvokers.Count);
+ // ConstructorInvokerInfo (public struct) has to be projected onto the adapter's internal
+ // ConstructorInvoker type; this is a representation change, not a defensive copy, and the
+ // parameter-type arrays are taken by reference.
+ var constructorInvokersMap = new Dictionary(constructorInvokers.Count);
foreach (KeyValuePair kvp in constructorInvokers)
{
var invokers = new SourceGeneratedReflectionDataProvider.ConstructorInvoker[kvp.Value.Length];
@@ -205,26 +208,20 @@ public static void Register(
ConstructorInvokerInfo info = kvp.Value[i];
invokers[i] = new SourceGeneratedReflectionDataProvider.ConstructorInvoker
{
- Parameters = (Type[])info.ParameterTypes.Clone(),
+ Parameters = info.ParameterTypes,
Invoker = info.Invoker,
};
}
- constructorInvokersCopy[kvp.Key] = invokers;
- }
-
- var propertySettersCopy = new Dictionary>(propertySetters.Count);
- foreach (KeyValuePair> kvp in propertySetters)
- {
- propertySettersCopy[kvp.Key] = kvp.Value;
+ constructorInvokersMap[kvp.Key] = invokers;
}
// TypesByName must always match Type.FullName at runtime (see comment in the source
// generator emitter): compute it on the runtime side from typeof(T).FullName so the
// generator emits less code and the same FullName conventions are honored for nested
// and generic types.
- var typesByName = new Dictionary(typesCopy.Length, StringComparer.Ordinal);
- foreach (Type type in typesCopy)
+ var typesByName = new Dictionary(types.Length, StringComparer.Ordinal);
+ foreach (Type type in types)
{
if (type.FullName is { } fullName)
{
@@ -236,14 +233,14 @@ public static void Register(
{
Assembly = assembly,
AssemblyName = assembly.GetName().Name ?? string.Empty,
- Types = typesCopy,
+ Types = types,
TypesByName = typesByName,
- TypeMethods = testMethodsCopy,
- TypeAttributes = typeAttributesCopy,
- AssemblyAttributes = assemblyAttributesCopy,
- TypeMethodInvokers = methodInvokersCopy,
- TypeConstructorsInvoker = constructorInvokersCopy,
- TypePropertySetters = propertySettersCopy,
+ TypeMethods = testMethodsMap,
+ TypeAttributes = typeAttributesMap,
+ AssemblyAttributes = assemblyAttributes,
+ TypeMethodInvokers = methodInvokersMap,
+ TypeConstructorsInvoker = constructorInvokersMap,
+ TypePropertySetters = propertySettersMap,
};
lock (Lock)
@@ -260,6 +257,27 @@ public static void Register(
}
}
+ // Reuses the caller-provided dictionary when it is already a concrete Dictionary<,> (the shape
+ // the source generator always emits), honoring the ownership-transfer contract with zero
+ // copying. Any other IReadOnlyDictionary implementation is materialized once so the provider
+ // still owns a concrete instance. Values are always taken by reference.
+ private static Dictionary AsOwnedDictionary(IReadOnlyDictionary source)
+ where TKey : notnull
+ {
+ if (source is Dictionary concrete)
+ {
+ return concrete;
+ }
+
+ var copy = new Dictionary(source.Count);
+ foreach (KeyValuePair kvp in source)
+ {
+ copy[kvp.Key] = kvp.Value;
+ }
+
+ return copy;
+ }
+
private static readonly Dictionary EmptyTypeAttributes = [];
private static readonly Dictionary> EmptyMethodInvokers = [];