diff --git a/src/LightInject.Tests/KeyedMicrosoftTests.cs b/src/LightInject.Tests/KeyedMicrosoftTests.cs index 4a35870c..872fa33e 100644 --- a/src/LightInject.Tests/KeyedMicrosoftTests.cs +++ b/src/LightInject.Tests/KeyedMicrosoftTests.cs @@ -210,10 +210,14 @@ public void ResolveKeyedServicesAnyKeyWithAnyKeyRegistration() container.TryGetInstance("something-else"); container.TryGetInstance("something-else-again"); - // Return all services registered with a non null key, but not the one "created" with KeyedService.AnyKey + // Return all services registered with a non null key, but not the one "created" with KeyedService.AnyKey, + // nor the KeyedService.AnyKey registration var allServices = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToList(); - Assert.Equal(5, allServices.Count); - Assert.Equal(new[] { service1, service2, service3, service4 }, allServices.Skip(1)); + Assert.Equal(4, allServices.Count); + Assert.Equal(new[] { service1, service2, service3, service4 }, allServices); + + var someKeyedServices = rootScope.GetInstance>("service").ToList(); + Assert.Equal(new[] { service2, service3, service4 }, someKeyedServices); } [Fact] @@ -332,14 +336,9 @@ public void ResolveKeyedServicesSingletonInstanceWithAnyKey() container.Register>(sf => service1, KeyedService.AnyKey.ToString(), new PerRootScopeLifetime(rootScope)); container.Register>(sf => service2, "some-key", new PerRootScopeLifetime(rootScope)); - // container.RegisterInstance>(service1, KeyedService.AnyKey.ToString()); - // container.RegisterInstance>(service2, "some-key"); - - - // var provider = CreateServiceProvider(serviceCollection); + // AnyKey registrations are not included when listing services by a specific key var services = rootScope.GetInstance>>("some-key").ToList(); - // var services = provider.GetKeyedServices>("some-key").ToList(); - Assert.Equal(new[] { service1, service2 }, services); + Assert.Equal(new[] { service2 }, services); } [Fact] @@ -692,6 +691,9 @@ public void ResolveKeyedSingletonFromScopeServiceProvider() Assert.Null(scopeA.TryGetInstance()); Assert.Null(scopeB.TryGetInstance()); + Assert.ThrowsAny(() => scopeA.GetInstance(KeyedService.AnyKey.ToString())); + Assert.ThrowsAny(() => scopeB.GetInstance(KeyedService.AnyKey.ToString())); + var serviceA1 = scopeA.GetInstance("key"); var serviceA2 = scopeA.GetInstance("key"); @@ -721,6 +723,9 @@ public void ResolveKeyedScopedFromScopeServiceProvider() Assert.Null(scopeA.TryGetInstance()); Assert.Null(scopeB.TryGetInstance()); + Assert.ThrowsAny(() => scopeA.GetInstance(KeyedService.AnyKey.ToString())); + Assert.ThrowsAny(() => scopeB.GetInstance(KeyedService.AnyKey.ToString())); + var serviceA1 = scopeA.GetInstance("key"); var serviceA2 = scopeA.GetInstance("key"); @@ -821,17 +826,337 @@ public void Test() var foo = rootScope.GetInstance("foo"); } - // [Fact] - // public void AnotherTest() - // { - // var serviceCollection = new ServiceCollection(); - // serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, (sp, key) => new Service((string)key)); + [Fact] + public void CombinationalRegistration() + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); - // Func factory = (sp, key) => new ServiceWithIntKey((int)key); + var keyedService1 = new Service(); + var keyedService2 = new Service(); + var nullkeyService1 = new Service(); + var nullkeyService2 = new Service(); - // factory(null, 32); - // } + container.RegisterInstance(nullkeyService1, null); + container.RegisterInstance(nullkeyService2, null); + container.RegisterInstance(keyedService1, "keyedService"); + container.RegisterInstance(keyedService2, "keyedService"); + + // Null key is treated as unkeyed - last registration wins for single resolution + Assert.Same(nullkeyService2, container.TryGetInstance()); + Assert.Same(nullkeyService2, container.TryGetInstance(null)); + + // AnyKey returns only specifically-keyed services (not null/unkeyed) + Assert.Equal( + new[] { keyedService1, keyedService2 }, + rootScope.GetInstance>(KeyedService.AnyKey.ToString())); + + // Specifically-keyed collection + Assert.Equal( + new[] { keyedService1, keyedService2 }, + rootScope.GetInstance>("keyedService")); + + // Single keyed - last registration wins + Assert.Same(keyedService2, rootScope.GetInstance("keyedService")); + } + + [Fact] + public void ResolveKeyedServicesAnyKeyConsistency() + { + var service = new Service("first-service"); + + // Container1: GetInstance(AnyKey single) before GetInstance(AnyKey) + { + var container1 = CreateContainer(); + var rootScope1 = container1.BeginScope(); + container1.RegisterInstance(service, "first-service"); + + Assert.Throws(() => rootScope1.GetInstance(KeyedService.AnyKey.ToString())); + Assert.Equal(new[] { service }, rootScope1.GetInstance>(KeyedService.AnyKey.ToString())); + } + + // Container2: GetInstance(AnyKey) before GetInstance(AnyKey single) + { + var container2 = CreateContainer(); + var rootScope2 = container2.BeginScope(); + container2.RegisterInstance(service, "first-service"); + + Assert.Equal(new[] { service }, rootScope2.GetInstance>(KeyedService.AnyKey.ToString())); + Assert.Throws(() => rootScope2.GetInstance(KeyedService.AnyKey.ToString())); + } + } + + [Fact] + public void ResolveKeyedServicesAnyKeyConsistencyWithAnyKeyRegistration() + { + var service = new Service("first-service"); + var any = new Service("any"); + + // Container1: GetInstance(AnyKey) before single key resolution + { + var container1 = CreateContainer(); + var rootScope1 = container1.BeginScope(); + container1.RegisterInstance(service, "first-service"); + container1.Register((factory, key) => any, KeyedService.AnyKey.ToString(), null); + + // AnyKey enumeration returns only specifically-registered services + Assert.Equal(new[] { service }, rootScope1.GetInstance>(KeyedService.AnyKey.ToString())); + } + + // Container2: single resolution before GetInstance(AnyKey) + { + var container2 = CreateContainer(); + var rootScope2 = container2.BeginScope(); + container2.RegisterInstance(service, "first-service"); + container2.Register((factory, key) => any, KeyedService.AnyKey.ToString(), null); + + Assert.Equal(new[] { service }, rootScope2.GetInstance>(KeyedService.AnyKey.ToString())); + // AnyKey registration acts as wildcard fallback for any unknown key + Assert.Same(any, rootScope2.GetInstance(new object().ToString())); + } + } + + [Fact] + public void ResolveKeyedServicesAnyKeyOrdering() + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); + + var service1 = new Service(); + var service2 = new Service(); + var service3 = new Service(); + + container.RegisterInstance(service1, "A-service"); + container.RegisterInstance(service2, "B-service"); + container.RegisterInstance(service3, "A-service"); + + // The order should be in registration order, not grouped by key + Assert.Equal( + new[] { service1, service2, service3 }, + rootScope.GetInstance>(KeyedService.AnyKey.ToString())); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_Constructor(bool anyKeyQueryBeforeSingletonQueries) + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); + + // Interweave different service types to check slot/ordering logic + container.Register(sf => new TestServiceA(), "key1", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceB(), "key1", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceA(), "key2", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceB(), "key2", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceA(), "key3", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceB(), "key3", new PerRootScopeLifetime(rootScope)); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + var serviceA1 = rootScope.GetInstance("key1"); + var serviceB1 = rootScope.GetInstance("key1"); + var serviceA2 = rootScope.GetInstance("key2"); + var serviceB2 = rootScope.GetInstance("key2"); + var serviceA3 = rootScope.GetInstance("key3"); + var serviceB3 = rootScope.GetInstance("key3"); + + if (!anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + Assert.Equal(new[] { serviceA1, serviceA2, serviceA3 }, allInstancesA); + Assert.Equal(new[] { serviceB1, serviceB2, serviceB3 }, allInstancesB); + + void DoAnyKeyQuery() + { + allInstancesA = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + allInstancesB = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_Constructor_Duplicates(bool anyKeyQueryBeforeSingletonQueries) + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); + + // Multiple registrations with the same key + container.Register(sf => new TestServiceA(), "key", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceB(), "key", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceA(), "key", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceB(), "key", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceA(), "key", new PerRootScopeLifetime(rootScope)); + container.Register(sf => new TestServiceB(), "key", new PerRootScopeLifetime(rootScope)); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + // Single resolution returns last registered (singleton) + var serviceA = rootScope.GetInstance("key"); + Assert.Same(serviceA, rootScope.GetInstance("key")); + + var serviceB = rootScope.GetInstance("key"); + Assert.Same(serviceB, rootScope.GetInstance("key")); + + if (!anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + // AnyKey enumeration returns all registrations + Assert.Equal(3, allInstancesA.Length); + Assert.Same(serviceA, allInstancesA[2]); + Assert.NotSame(serviceA, allInstancesA[1]); + Assert.NotSame(serviceA, allInstancesA[0]); + Assert.NotSame(allInstancesA[0], allInstancesA[1]); + + Assert.Equal(3, allInstancesB.Length); + Assert.Same(serviceB, allInstancesB[2]); + Assert.NotSame(serviceB, allInstancesB[1]); + Assert.NotSame(serviceB, allInstancesB[0]); + Assert.NotSame(allInstancesB[0], allInstancesB[1]); + + void DoAnyKeyQuery() + { + allInstancesA = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + allInstancesB = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_InstanceProvided(bool anyKeyQueryBeforeSingletonQueries) + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); + + var serviceA1 = new TestServiceA(); + var serviceA2 = new TestServiceA(); + var serviceA3 = new TestServiceA(); + var serviceB1 = new TestServiceB(); + var serviceB2 = new TestServiceB(); + var serviceB3 = new TestServiceB(); + + // Interweave to check slot/ordering logic + container.RegisterInstance(serviceA1, "key1"); + container.RegisterInstance(serviceB1, "key1"); + container.RegisterInstance(serviceA2, "key2"); + container.RegisterInstance(serviceB2, "key2"); + container.RegisterInstance(serviceA3, "key3"); + container.RegisterInstance(serviceB3, "key3"); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + Assert.Same(serviceA1, rootScope.GetInstance("key1")); + Assert.Same(serviceA2, rootScope.GetInstance("key2")); + Assert.Same(serviceA3, rootScope.GetInstance("key3")); + Assert.Same(serviceB1, rootScope.GetInstance("key1")); + Assert.Same(serviceB2, rootScope.GetInstance("key2")); + Assert.Same(serviceB3, rootScope.GetInstance("key3")); + + if (!anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + Assert.Equal(new[] { serviceA1, serviceA2, serviceA3 }, allInstancesA); + Assert.Equal(new[] { serviceB1, serviceB2, serviceB3 }, allInstancesB); + + void DoAnyKeyQuery() + { + allInstancesA = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + allInstancesB = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + Assert.Equal(allInstancesA, rootScope.GetInstance>(KeyedService.AnyKey.ToString())); + Assert.Equal(allInstancesB, rootScope.GetInstance>(KeyedService.AnyKey.ToString())); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResolveWithAnyKeyQuery_InstanceProvided_Duplicates(bool anyKeyQueryBeforeSingletonQueries) + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); + + var serviceA1 = new TestServiceA(); + var serviceA2 = new TestServiceA(); + var serviceA3 = new TestServiceA(); + var serviceB1 = new TestServiceB(); + var serviceB2 = new TestServiceB(); + var serviceB3 = new TestServiceB(); + + // Multiple registrations with the same key, interleaved + container.RegisterInstance(serviceA1, "key"); + container.RegisterInstance(serviceB1, "key"); + container.RegisterInstance(serviceA2, "key"); + container.RegisterInstance(serviceB2, "key"); + container.RegisterInstance(serviceA3, "key"); + container.RegisterInstance(serviceB3, "key"); + + TestServiceA[] allInstancesA = null; + TestServiceB[] allInstancesB = null; + + if (anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + // Single resolution returns the last registered for duplicates + Assert.Same(serviceA3, rootScope.GetInstance("key")); + Assert.Same(serviceB3, rootScope.GetInstance("key")); + if (!anyKeyQueryBeforeSingletonQueries) + DoAnyKeyQuery(); + + Assert.Equal(new[] { serviceA1, serviceA2, serviceA3 }, allInstancesA); + Assert.Equal(new[] { serviceB1, serviceB2, serviceB3 }, allInstancesB); + + void DoAnyKeyQuery() + { + allInstancesA = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + allInstancesB = rootScope.GetInstance>(KeyedService.AnyKey.ToString()).ToArray(); + Assert.Equal(allInstancesA, rootScope.GetInstance>(KeyedService.AnyKey.ToString())); + Assert.Equal(allInstancesB, rootScope.GetInstance>(KeyedService.AnyKey.ToString())); + } + } + + [Fact] + public void ResolveRequiredKeyedServiceThrowsIfNotFound() + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); + var serviceKey = "non-existent-key"; + + Assert.Throws(() => rootScope.GetInstance(serviceKey)); + Assert.Throws(() => rootScope.GetInstance(typeof(IService), serviceKey)); + } + + [Fact] + public void SimpleServiceKeyedResolution() + { + var container = CreateContainer(); + var rootScope = container.BeginScope(); + + container.Register("simple"); + container.Register("another"); + container.Register(); + container.RegisterInstance(container); + + var sut = rootScope.GetInstance(); + + var result = sut.GetService("simple"); + + Assert.True(result.GetType() == typeof(SimpleService)); + } public interface IKeyedService { @@ -926,6 +1251,27 @@ public OtherService( public IService Service2 { get; } } + internal class TestServiceA { } + + internal class TestServiceB { } + + public interface ISimpleService { } + + public class SimpleService : ISimpleService { } + + public class AnotherSimpleService : ISimpleService { } + + public class SimpleParentWithDynamicKeyedService + { + private readonly IServiceFactory _serviceFactory; + + public SimpleParentWithDynamicKeyedService(IServiceFactory serviceFactory) + { + _serviceFactory = serviceFactory; + } + + public ISimpleService GetService(string name) => _serviceFactory.GetInstance(name); + } } diff --git a/src/LightInject/LightInject.cs b/src/LightInject/LightInject.cs index de0f1988..cba279bf 100644 --- a/src/LightInject/LightInject.cs +++ b/src/LightInject/LightInject.cs @@ -4846,12 +4846,12 @@ private Action CreateEmitMethodForEnumerableServiceServiceRequest(Type { if (serviceName == "*") { - emitMethods = allEmitters.Keys.Where(k => actualServiceType.IsAssignableFrom(k.ServiceType) && k.ServiceName.Length > 0).SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); + emitMethods = allEmitters.Keys.Where(k => actualServiceType.IsAssignableFrom(k.ServiceType) && k.ServiceName.Length > 0 && k.ServiceName != "*").SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); } else if (serviceName.Length > 0) { - emitMethods = allEmitters.Keys.Where(k => actualServiceType.IsAssignableFrom(k.ServiceType) && (k.ServiceName == serviceName || k.ServiceName == "*")).SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); + emitMethods = allEmitters.Keys.Where(k => actualServiceType.IsAssignableFrom(k.ServiceType) && k.ServiceName == serviceName).SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); } else { @@ -4873,12 +4873,12 @@ private Action CreateEmitMethodForEnumerableServiceServiceRequest(Type { if (serviceName == "*") { - emitMethods = allEmitters.Keys.Where(k => actualServiceType == k.ServiceType && k.ServiceName.Length > 0).SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); + emitMethods = allEmitters.Keys.Where(k => actualServiceType == k.ServiceType && k.ServiceName.Length > 0 && k.ServiceName != "*").SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); } else if (serviceName.Length > 0) { - emitMethods = allEmitters.Keys.Where(k => actualServiceType == k.ServiceType && (k.ServiceName == serviceName || k.ServiceName == "*")).SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); + emitMethods = allEmitters.Keys.Where(k => actualServiceType == k.ServiceType && k.ServiceName == serviceName).SelectMany(k => allEmitters[k]).Where(emi => !emi.CreatedFromWildcardService).OrderBy(emi => emi.RegistrationOrder).Select(emi => emi.EmitMethod).ToList(); } else { diff --git a/src/LightInject/LightInject.csproj b/src/LightInject/LightInject.csproj index f7b7d426..c6ef5fb3 100644 --- a/src/LightInject/LightInject.csproj +++ b/src/LightInject/LightInject.csproj @@ -2,7 +2,7 @@ net8.0;netstandard2.0;net462 - 7.0.2 + 7.1.0 Bernhard Richter https://www.lightinject.net git