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