From e9453ef57e1f3d1c892dbd9213beff84ad86cd0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:52:22 +0000 Subject: [PATCH 1/4] Initial plan From 190244aebfd9f72116fe3e259096eb088483855b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:42:29 +0000 Subject: [PATCH 2/4] Fix NullReferenceException in MultiThreadedGetExportsWorkWithImportingConstructor by caching GetParameters() result On Mono/ARM, concurrent calls to ConstructorInfo.GetParameters() on the same ConstructorInfo can return different ParameterInfo instances due to lazy initialization. When GetActivator called GetParameters() a second time, the resulting ParameterInfo objects could differ from those stored in dependencies (from the first GetParameters() call in GetPartActivatorDependencies), causing ParameterInfoComparer.GetHashCode to fail (e.g., null Member on ARM) or the dictionary lookup to fail silently, leading to wrong/null constructor arguments. Fix: cache the GetParameters() result in _constructorParameters after the first call in GetPartActivatorDependencies, and reuse it in GetActivator to ensure the exact same ParameterInfo instances are used for both building the dependency dictionary and iterating over constructor parameters. Fixes: https://github.com/dotnet/runtime/issues/MultiThreadedGetExportsWorkWithImportingConstructor Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com> --- .../TypedParts/Discovery/DiscoveredPart.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs b/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs index 88f005d09ed1e4..b83b134d5150b6 100644 --- a/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs +++ b/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs @@ -31,6 +31,7 @@ internal sealed class DiscoveredPart // Lazily initialised among potentially many exports private ConstructorInfo _constructor; + private ParameterInfo[] _constructorParameters; private CompositeActivator _partActivator; private static readonly IDictionary s_noMetadata = new Dictionary(); @@ -112,7 +113,13 @@ private IEnumerable GetPartActivatorDependencies(Dependen } } - var cps = _constructor.GetParameters(); + // Cache GetParameters() result to ensure the same ParameterInfo instances + // are used in both GetPartActivatorDependencies and GetActivator. + // On some runtimes (e.g. Mono on ARM), concurrent calls to GetParameters() + // on the same ConstructorInfo can return different ParameterInfo instances + // due to lazy initialization, causing lookup failures. + _constructorParameters = _constructor.GetParameters(); + var cps = _constructorParameters; for (var i = 0; i < cps.Length; ++i) { @@ -168,7 +175,7 @@ public CompositeActivator GetActivator(IEnumerable depend var contextParam = Expression.Parameter(typeof(LifetimeContext), "cc"); var operationParm = Expression.Parameter(typeof(CompositionOperation), "op"); - var cps = _constructor.GetParameters(); + var cps = _constructorParameters ?? _constructor.GetParameters(); Expression[] paramActivatorCalls = new Expression[cps.Length]; var partActivatorDependencies = dependencies From 5d93104fd7be13c472785ef710371e3b6c10b59b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:34:40 +0000 Subject: [PATCH 3/4] Changes before error encountered Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com> --- .../TypedParts/Discovery/DiscoveredPart.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs b/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs index b83b134d5150b6..e939c1f6a997e5 100644 --- a/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs +++ b/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs @@ -12,6 +12,7 @@ using System.Linq.Expressions; using System.Numerics.Hashing; using System.Reflection; +using System.Threading; namespace System.Composition.TypedParts.Discovery { @@ -113,13 +114,12 @@ private IEnumerable GetPartActivatorDependencies(Dependen } } - // Cache GetParameters() result to ensure the same ParameterInfo instances - // are used in both GetPartActivatorDependencies and GetActivator. + // Use LazyInitializer to ensure GetParameters() is called at most once per + // DiscoveredPart instance and the result is safely published to all threads. // On some runtimes (e.g. Mono on ARM), concurrent calls to GetParameters() // on the same ConstructorInfo can return different ParameterInfo instances - // due to lazy initialization, causing lookup failures. - _constructorParameters = _constructor.GetParameters(); - var cps = _constructorParameters; + // due to lazy initialization, causing lookup failures in GetActivator. + var cps = LazyInitializer.EnsureInitialized(ref _constructorParameters, () => _constructor.GetParameters()); for (var i = 0; i < cps.Length; ++i) { @@ -175,7 +175,7 @@ public CompositeActivator GetActivator(IEnumerable depend var contextParam = Expression.Parameter(typeof(LifetimeContext), "cc"); var operationParm = Expression.Parameter(typeof(CompositionOperation), "op"); - var cps = _constructorParameters ?? _constructor.GetParameters(); + var cps = LazyInitializer.EnsureInitialized(ref _constructorParameters, () => _constructor.GetParameters()); Expression[] paramActivatorCalls = new Expression[cps.Length]; var partActivatorDependencies = dependencies From b60ceee81288422903d8e0876a9baa97e35b0834 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:42:18 +0000 Subject: [PATCH 4/4] Fix IDE0200: use method group in GetActivator for LazyInitializer call Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com> --- .../System/Composition/TypedParts/Discovery/DiscoveredPart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs b/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs index e939c1f6a997e5..5f8e9d05b1264d 100644 --- a/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs +++ b/src/libraries/System.Composition.TypedParts/src/System/Composition/TypedParts/Discovery/DiscoveredPart.cs @@ -175,7 +175,7 @@ public CompositeActivator GetActivator(IEnumerable depend var contextParam = Expression.Parameter(typeof(LifetimeContext), "cc"); var operationParm = Expression.Parameter(typeof(CompositionOperation), "op"); - var cps = LazyInitializer.EnsureInitialized(ref _constructorParameters, () => _constructor.GetParameters()); + var cps = LazyInitializer.EnsureInitialized(ref _constructorParameters, _constructor.GetParameters); Expression[] paramActivatorCalls = new Expression[cps.Length]; var partActivatorDependencies = dependencies