From 16b028e35b01a4807deb36866587b0318777d0cc Mon Sep 17 00:00:00 2001 From: Bernhard Richter Date: Fri, 25 Sep 2026 01:00:43 +0200 Subject: [PATCH] Synchronize CanGetInstance with delegate creation CanGetInstance may register closed generic services while resolving the emit method. Without taking the container lock this raced with CreateDelegate enumerating the same registration lists, causing "Collection was modified" exceptions under load (e.g. when used as IServiceProviderIsService by ActivatorUtilities). Fixes #614 Co-Authored-By: Claude Opus 5.5 --- src/LightInject.Tests/Issue614.cs | 77 +++++++++++++++++++++++++++++++ src/LightInject/LightInject.cs | 15 ++++-- 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 src/LightInject.Tests/Issue614.cs diff --git a/src/LightInject.Tests/Issue614.cs b/src/LightInject.Tests/Issue614.cs new file mode 100644 index 00000000..3791d110 --- /dev/null +++ b/src/LightInject.Tests/Issue614.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace LightInject.Tests +{ + public class Issue614 + { + [Fact] + public void ShouldResolveOpenGenericEnumerablesConcurrentlyWithCanGetInstance() + { + var closingTypes = typeof(string).Assembly.GetExportedTypes() + .Where(t => t.IsClass && !t.IsGenericTypeDefinition && !t.ContainsGenericParameters) + .Take(200) + .ToArray(); + + for (int iteration = 0; iteration < 20; iteration++) + { + var container = new ServiceContainer(new ContainerOptions { EnableMicrosoftCompatibility = true, EnableVariance = false }); + container.Register(typeof(IBehavior<>), typeof(FirstBehavior<>)); + container.Register(typeof(IBehavior<>), typeof(SecondBehavior<>)); + container.Register(typeof(Handler<>), typeof(Handler<>)); + + using var barrier = new Barrier(Environment.ProcessorCount * 2); + var tasks = Enumerable.Range(0, Environment.ProcessorCount * 2).Select(taskIndex => Task.Run(() => + { + barrier.SignalAndWait(); + for (int i = taskIndex; i < closingTypes.Length; i += 2) + { + var closingType = closingTypes[i]; + if (taskIndex % 2 == 0) + { + container.CanGetInstance(typeof(IEnumerable<>).MakeGenericType(typeof(IBehavior<>).MakeGenericType(closingType)), string.Empty); + } + else + { + var handler = (IHandler)container.GetInstance(typeof(Handler<>).MakeGenericType(closingType)); + Assert.NotNull(handler); + } + } + })).ToArray(); + + Task.WaitAll(tasks); + } + } + + public interface IBehavior + { + } + + public class FirstBehavior : IBehavior + { + } + + public class SecondBehavior : IBehavior + { + } + + public interface IHandler + { + int BehaviorCount { get; } + } + + public class Handler : IHandler + { + public Handler(IEnumerable> behaviors) + { + BehaviorCount = behaviors.Count(); + } + + public int BehaviorCount { get; } + } + } +} diff --git a/src/LightInject/LightInject.cs b/src/LightInject/LightInject.cs index cba279bf..d8278473 100644 --- a/src/LightInject/LightInject.cs +++ b/src/LightInject/LightInject.cs @@ -2836,13 +2836,18 @@ public IEnumerable AvailableServices /// public bool CanGetInstance(Type serviceType, string serviceName) { - if (serviceType.IsFuncRepresentingService() || serviceType.IsFuncRepresentingNamedService() || serviceType.IsFuncWithParameters() || serviceType.IsLazy()) + // Resolving the emit method might register services (e.g. closed generics), so we need to + // synchronize with CreateDelegate that reads the same registrations. + lock (lockObject) { - var returnType = serviceType.GenericTypeArguments.Last(); - return GetEmitMethod(returnType, serviceName) != null || availableServices.ContainsKey(serviceType); - } + if (serviceType.IsFuncRepresentingService() || serviceType.IsFuncRepresentingNamedService() || serviceType.IsFuncWithParameters() || serviceType.IsLazy()) + { + var returnType = serviceType.GenericTypeArguments.Last(); + return GetEmitMethod(returnType, serviceName) != null || availableServices.ContainsKey(serviceType); + } - return GetEmitMethod(serviceType, serviceName) != null; + return GetEmitMethod(serviceType, serviceName) != null; + } } ///