From bd0c5823be751f8793edadbad9e734513080af7c Mon Sep 17 00:00:00 2001 From: Bernhard Richter Date: Fri, 25 Sep 2026 01:18:03 +0200 Subject: [PATCH] Only close open generic registrations once per closed service type With Microsoft compatibility enabled, every enumerable request for the same closed generic service type closed the open generic registrations again and appended them to the list of emitters, resulting in duplicate items (e.g. IEnumerable> returning 4 items instead of 2). The same happened when a closed generic had already been registered through a single service request. Co-Authored-By: Claude Opus 5.5 --- .../ClosedGenericEnumerableTests.cs | 84 +++++++++++++++++++ src/LightInject/LightInject.cs | 31 ++++++- 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/LightInject.Tests/ClosedGenericEnumerableTests.cs diff --git a/src/LightInject.Tests/ClosedGenericEnumerableTests.cs b/src/LightInject.Tests/ClosedGenericEnumerableTests.cs new file mode 100644 index 00000000..314e107f --- /dev/null +++ b/src/LightInject.Tests/ClosedGenericEnumerableTests.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace LightInject.Tests +{ + public class ClosedGenericEnumerableTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ShouldNotDuplicateClosedGenericServicesWhenEnumerableIsResolvedMultipleTimes(bool enableMicrosoftCompatibility) + { + var container = CreateContainer(enableMicrosoftCompatibility); + + var handler = container.GetInstance>(); + var behaviors = container.GetInstance>>(); + + Assert.Equal(new[] { typeof(FirstBehavior), typeof(SecondBehavior) }, handler.Behaviors.Select(b => b.GetType())); + Assert.Equal(new[] { typeof(FirstBehavior), typeof(SecondBehavior) }, behaviors.Select(b => b.GetType())); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ShouldNotDuplicateClosedGenericServicesAfterCanGetInstance(bool enableMicrosoftCompatibility) + { + var container = CreateContainer(enableMicrosoftCompatibility); + + Assert.True(container.CanGetInstance(typeof(IEnumerable>), string.Empty)); + var behaviors = container.GetInstance>>(); + + Assert.Equal(new[] { typeof(FirstBehavior), typeof(SecondBehavior) }, behaviors.Select(b => b.GetType())); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ShouldNotDuplicateClosedGenericServiceResolvedAsSingleServiceBeforeEnumerable(bool enableMicrosoftCompatibility) + { + var container = CreateContainer(enableMicrosoftCompatibility); + + var behavior = container.GetInstance>(enableMicrosoftCompatibility ? string.Empty : "second"); + var behaviors = container.GetInstance>>(); + + Assert.IsType>(behavior); + Assert.Equal(new[] { typeof(FirstBehavior), typeof(SecondBehavior) }, behaviors.Select(b => b.GetType())); + } + + private static ServiceContainer CreateContainer(bool enableMicrosoftCompatibility) + { + var container = new ServiceContainer(new ContainerOptions { EnableMicrosoftCompatibility = enableMicrosoftCompatibility, EnableVariance = false }); + container.Register(typeof(IBehavior<>), typeof(FirstBehavior<>)); + + // Without Microsoft compatibility, registering another default service would replace the first one. + container.Register(typeof(IBehavior<>), typeof(SecondBehavior<>), enableMicrosoftCompatibility ? string.Empty : "second"); + container.Register(typeof(BehaviorHandler<>), typeof(BehaviorHandler<>)); + return container; + } + + public interface IBehavior + { + } + + public class FirstBehavior : IBehavior + { + } + + public class SecondBehavior : IBehavior + { + } + + public class BehaviorHandler + { + public BehaviorHandler(IEnumerable> behaviors) + { + Behaviors = behaviors.ToArray(); + } + + public IBehavior[] Behaviors { get; } + } + } +} diff --git a/src/LightInject/LightInject.cs b/src/LightInject/LightInject.cs index cba279bf..928505e3 100644 --- a/src/LightInject/LightInject.cs +++ b/src/LightInject/LightInject.cs @@ -2620,6 +2620,7 @@ public class ServiceContainer : IServiceContainer private readonly ServiceRegistry availableServices = new ServiceRegistry(); private readonly ConcurrentDictionary> allRegistrations = new ConcurrentDictionary>(); private readonly ConcurrentDictionary> allEmitters = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> closedGenericRegistrationOrders = new ConcurrentDictionary>(); private readonly object lockObject = new object(); private readonly ContainerOptions options; @@ -4747,7 +4748,7 @@ private Action CreateEmitMethodBasedOnClosedGenericServiceRequest(Type // Ensure that we only add candidates that are assignable to the requested service type. if (closedGenericServiceType.IsAssignableFrom(closedGenericImplementingTypeCandidate)) { - candidates.Add(openGenericServiceRegistration.ServiceName, new ClosedGenericCandidate(closedGenericImplementingTypeCandidate, openGenericServiceRegistration.Lifetime)); + candidates.Add(openGenericServiceRegistration.ServiceName, new ClosedGenericCandidate(closedGenericImplementingTypeCandidate, openGenericServiceRegistration.Lifetime, openGenericServiceRegistration.ServiceName, openGenericServiceRegistration.RegistrationOrder)); } } } @@ -4799,10 +4800,25 @@ Action RegisterAndGetEmitMethod() Lifetime = CloneLifeTime(candidate.Lifetime) ?? DefaultLifetime, }; Register(serviceRegistration); + if (string.Equals(serviceName, candidate.ServiceName, StringComparison.OrdinalIgnoreCase)) + { + TryMarkAsClosed(closedGenericServiceType, candidate.RegistrationOrder); + } + return GetEmitMethod(serviceRegistration.ServiceType, serviceRegistration.ServiceName); } } + // Ensures that an open generic registration is only closed once for a given closed generic service type. + private bool TryMarkAsClosed(Type closedGenericServiceType, int openGenericRegistrationOrder) + { + var registrationOrders = closedGenericRegistrationOrders.GetOrAdd(closedGenericServiceType, _ => new HashSet()); + lock (registrationOrders) + { + return registrationOrders.Add(openGenericRegistrationOrder); + } + } + private Action CreateEmitMethodForEnumerableServiceServiceRequest(Type serviceType, string serviceName) { Type actualServiceType = TypeHelper.GetElementType(serviceType); @@ -4825,6 +4841,11 @@ private Action CreateEmitMethodForEnumerableServiceServiceRequest(Type foreach (var constructableOpenGenericService in constructableOpenGenericServices) { + if (!TryMarkAsClosed(actualServiceType, constructableOpenGenericService.RegistrationOrder)) + { + continue; + } + var serviceRegistration = new ServiceRegistration { ServiceType = actualServiceType, @@ -5288,15 +5309,21 @@ private void RegisterValue(Type serviceType, object value, string serviceName) private struct ClosedGenericCandidate { - public ClosedGenericCandidate(Type closedGenericImplementingType, ILifetime lifetime) + public ClosedGenericCandidate(Type closedGenericImplementingType, ILifetime lifetime, string serviceName, int registrationOrder) { ClosedGenericImplementingType = closedGenericImplementingType; Lifetime = lifetime; + ServiceName = serviceName; + RegistrationOrder = registrationOrder; } public Type ClosedGenericImplementingType { get; } public ILifetime Lifetime { get; } + + public string ServiceName { get; } + + public int RegistrationOrder { get; } } private class Storage