diff --git a/src/Ninject.Web.AspNetCore.ComplianceTest/DependencyInjectionComplianceTests.cs b/src/Ninject.Web.AspNetCore.ComplianceTest/DependencyInjectionComplianceTests.cs index 52a94da..2391f3a 100644 --- a/src/Ninject.Web.AspNetCore.ComplianceTest/DependencyInjectionComplianceTests.cs +++ b/src/Ninject.Web.AspNetCore.ComplianceTest/DependencyInjectionComplianceTests.cs @@ -1,27 +1,26 @@ using Microsoft.Extensions.DependencyInjection; using System; -namespace Ninject.Web.AspNetCore.ComplianceTests +namespace Ninject.Web.AspNetCore.ComplianceTest; + +/// +/// See https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src - the dotnet/runtime +/// project which contains the dependency injection library code also contains a set of "compliance tests" that can be run against a potential alternative +/// implementation to check if it is compliant. This class here is doing just that. +/// +/// The project also contains a separate test project that includes these compliance tests for a set of compliant third party DI implementations like +/// Autofac and Lightinject under https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests. +/// +/// All of this is part of the https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln +/// solution of dotnet/runtime. +/// +public class DependencyInjectionComplianceTests : Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests { - /// - /// See https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src - the dotnet/runtime - /// project which contains the dependency injection library code also contains a set of "compliance tests" that can be run against a potential alternative - /// implementation to check if it is compliant. This class here is doing just that. - /// - /// The project also contains a separate test project that includes these compliance tests for a set of compliant third party DI implementations like - /// Autofac and Lightinject under https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests. - /// - /// All of this is part of the https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln - /// solution of dotnet/runtime. - /// - public class DependencyInjectionComplianceTests : Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests + protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) { - protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) - { - var kernel = new AspNetCoreKernel(); - var factory = new NinjectServiceProviderFactory(kernel); + var kernel = new AspNetCoreKernel(); + var factory = new NinjectServiceProviderFactory(kernel); - return factory.CreateBuilder(serviceCollection).Build(); - } + return factory.CreateBuilder(serviceCollection).Build(); } } diff --git a/src/Ninject.Web.AspNetCore.ComplianceTest/KeyedDependencyInjectionComplianceTests.cs b/src/Ninject.Web.AspNetCore.ComplianceTest/KeyedDependencyInjectionComplianceTests.cs new file mode 100644 index 0000000..934d3e3 --- /dev/null +++ b/src/Ninject.Web.AspNetCore.ComplianceTest/KeyedDependencyInjectionComplianceTests.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Ninject.Planning.Bindings.Resolvers; +using Xunit; + +namespace Ninject.Web.AspNetCore.ComplianceTest; + +/// +/// See https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src - the dotnet/runtime +/// project which contains the dependency injection library code also contains a set of "compliance tests" that can be run against a potential alternative +/// implementation to check if it is compliant. This class is running the dedicated specification tests for KEYED services. +/// +public class KeyedDependencyInjectionComplianceTests : Microsoft.Extensions.DependencyInjection.Specification.KeyedDependencyInjectionSpecificationTests +{ + protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) + { + var kernel = new AspNetCoreKernel(); + // remove autobinding as CreateServiceWithKeyedParameter e.g. tests that no autobinding happens. + kernel.Components.Remove(); + var factory = new NinjectServiceProviderFactory(kernel); + + return factory.CreateBuilder(serviceCollection).Build(); + } + +#pragma warning disable xUnit1024, xUnit1026 + + [Theory(Skip = "Wrong implementation of the test, should use Assert.Equal and not Assert.Same")] + [InlineData(true)] + [InlineData(false)] + public new void ResolveWithAnyKeyQuery_Constructor(bool anyKeyQueryBeforeSingletonQueries) + { + } + + [Theory(Skip = "Wrong implementation, should use Assert.Equal and not Assert.Same")] + [InlineData(true)] + [InlineData(false)] + public new void ResolveWithAnyKeyQuery_Constructor_Duplicates(bool anyKeyQueryBeforeSingletonQueries) + { + } +#pragma warning restore xUnit1024, xUnit1026 +} diff --git a/src/Ninject.Web.AspNetCore.Test/Fakes/IKeyedWeaponStorage.cs b/src/Ninject.Web.AspNetCore.Test/Fakes/IKeyedWeaponStorage.cs new file mode 100644 index 0000000..08c5bfd --- /dev/null +++ b/src/Ninject.Web.AspNetCore.Test/Fakes/IKeyedWeaponStorage.cs @@ -0,0 +1,9 @@ +namespace Ninject.Web.AspNetCore.Test.Fakes +{ +#if NET8_0_OR_GREATER + public interface IKeyedWeaponStorage + { + IWeapon Weapon { get; } + } +#endif +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore.Test/Fakes/KeyedNinja.cs b/src/Ninject.Web.AspNetCore.Test/Fakes/KeyedNinja.cs new file mode 100644 index 0000000..a520a6e --- /dev/null +++ b/src/Ninject.Web.AspNetCore.Test/Fakes/KeyedNinja.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Ninject.Web.AspNetCore.Test.Fakes +{ +#if NET8_0_OR_GREATER + public class KeyedNinja : IWarrior + { + public object Key {get; private set;} + + public KeyedNinja([ServiceKey] object key) + { + Key = key; + } + + public string Name => nameof(KeyedNinja); + } +#endif +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore.Test/Fakes/KeyedWeaponStorage.cs b/src/Ninject.Web.AspNetCore.Test/Fakes/KeyedWeaponStorage.cs new file mode 100644 index 0000000..7665c2b --- /dev/null +++ b/src/Ninject.Web.AspNetCore.Test/Fakes/KeyedWeaponStorage.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Ninject.Web.AspNetCore.Test.Fakes +{ +#if NET8_0_OR_GREATER + public class KeyedWeaponStorage : IKeyedWeaponStorage + { + public IWeapon Weapon { get; private set; } + public KeyedWeaponStorage([FromKeyedServices("Lance")] IWeapon lance) + { + Weapon = lance; + } + } +#endif +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore.Test/Fakes/NinjaWithKeyedWeapon.cs b/src/Ninject.Web.AspNetCore.Test/Fakes/NinjaWithKeyedWeapon.cs new file mode 100644 index 0000000..1eb4e18 --- /dev/null +++ b/src/Ninject.Web.AspNetCore.Test/Fakes/NinjaWithKeyedWeapon.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Ninject.Web.AspNetCore.Test.Fakes +{ +#if NET8_0_OR_GREATER + + public class NinjaWithKeyedWeapon : IWarrior + { + public IKeyedWeaponStorage Storage { get; private set; } + public IWeapon Weapon { get; private set; } + + public string Name => nameof(NinjaWithKeyedWeapon) + $" with weapon {Weapon.Type}"; + + public NinjaWithKeyedWeapon([FromKeyedServices("Longsword")] IWeapon weapon, [FromKeyedServices("Storage")] IKeyedWeaponStorage storage) + { + Weapon = weapon; + Storage = storage; + } + } + +#endif +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore.Test/Unit/DuplicateDescriptorTest.cs b/src/Ninject.Web.AspNetCore.Test/Unit/DuplicateDescriptorTest.cs index c3fef70..cedbe1d 100644 --- a/src/Ninject.Web.AspNetCore.Test/Unit/DuplicateDescriptorTest.cs +++ b/src/Ninject.Web.AspNetCore.Test/Unit/DuplicateDescriptorTest.cs @@ -66,7 +66,15 @@ public void BindingsFromKernel_ResolveWarrior_FailsWithActivationException(Resol else { Action action = () => resolver.Resolve(kernel); - action.Should().Throw().WithMessage("Error activating IWarrior*"); + if (resolver.ResolveType == ResolveType.ServiceProviderRequired) + { + action.Should().Throw().WithInnerException() + .WithMessage("Error activating IWarrior*"); + } + else + { + action.Should().Throw().WithMessage("Error activating IWarrior*"); + } } } @@ -99,7 +107,15 @@ public void BindingsMixedWarriorDescriptors_ResolveWarrior_FailsWithActivationEx else { Action action = () => resolver.Resolve(kernel); - action.Should().Throw().WithMessage("Error activating IWeapon*"); + if (resolver.ResolveType == ResolveType.ServiceProviderRequired) + { + action.Should().Throw().WithInnerException() + .WithMessage("Error activating IWeapon*"); + } + else + { + action.Should().Throw().WithMessage("Error activating IWeapon*"); + } } } diff --git a/src/Ninject.Web.AspNetCore.Test/Unit/IndexedBindingPrecedenceComparerTest.cs b/src/Ninject.Web.AspNetCore.Test/Unit/IndexedBindingPrecedenceComparerTest.cs index c62c250..7c7ecfc 100644 --- a/src/Ninject.Web.AspNetCore.Test/Unit/IndexedBindingPrecedenceComparerTest.cs +++ b/src/Ninject.Web.AspNetCore.Test/Unit/IndexedBindingPrecedenceComparerTest.cs @@ -86,7 +86,7 @@ public DummyBinding() public DummyBinding WithIndex(BindingIndex index) { - Metadata.Set(nameof(BindingIndex), index.Next(Service)); + Metadata.Set(nameof(BindingIndex), index.Next(Service, BindingIndex.UnkeyedIndexKey.Instance)); return this; } diff --git a/src/Ninject.Web.AspNetCore.Test/Unit/ServiceProviderKeyedTest.cs b/src/Ninject.Web.AspNetCore.Test/Unit/ServiceProviderKeyedTest.cs new file mode 100644 index 0000000..da93fa7 --- /dev/null +++ b/src/Ninject.Web.AspNetCore.Test/Unit/ServiceProviderKeyedTest.cs @@ -0,0 +1,255 @@ +using AwesomeAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Ninject.Web.AspNetCore.Test.Fakes; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Ninject.Web.AspNetCore.Test.Unit +{ + +#if NET8_0_OR_GREATER + + public class ServiceProviderKeyedTest + { + + [Fact] + public void OptionalExising_ServiceKeyNullResolvedAsUnkeyed() + { + var collection = new ServiceCollection(); + collection.Add(new ServiceDescriptor(typeof(IWarrior),null, typeof(Samurai), ServiceLifetime.Transient)); + var kernel = CreateTestKernel(collection); + var provider = CreateServiceProvider(kernel); + + var warrior = provider.GetKeyedService(typeof(Samurai), null); + warrior.Should().NotBeNull().And.BeOfType(typeof(Samurai)); + } + + [Fact] + public void OptionalExising_SingleServiceInjectedServiceKeyResolved() + { + var collection = new ServiceCollection(); + collection.Add(new ServiceDescriptor(typeof(IWarrior), "Ninja", typeof(KeyedNinja), ServiceLifetime.Transient)); + var kernel = CreateTestKernel(collection); + var provider = CreateServiceProvider(kernel); + + var warrior = provider.GetKeyedService(typeof(IWarrior), "Ninja"); + warrior.Should().NotBeNull().And.BeOfType(typeof(KeyedNinja)).And.Match(x => ((KeyedNinja)x).Key.ToString() == "Ninja"); + } + + [Fact] + public void OptionalExisingWithKeyedChildren_SingleServiceResolved() + { + var collection = new ServiceCollection(); + collection.Add(new ServiceDescriptor(typeof(IWarrior), "Ninja", typeof(NinjaWithKeyedWeapon), ServiceLifetime.Transient)); + collection.Add(new ServiceDescriptor(typeof(IKeyedWeaponStorage), "Storage", typeof(KeyedWeaponStorage), ServiceLifetime.Transient)); + collection.Add(new ServiceDescriptor(typeof(IWeapon), "Longsword", typeof(Longsword), ServiceLifetime.Transient)); + collection.Add(new ServiceDescriptor(typeof(IWeapon), "Lance", typeof(Lance), ServiceLifetime.Transient)); + var kernel = CreateTestKernel(collection); + var provider = CreateServiceProvider(kernel); + + var warrior = provider.GetKeyedService(typeof(IWarrior), "Ninja"); + warrior.Should().NotBeNull().And.BeOfType(typeof(NinjaWithKeyedWeapon)).And.Match(x => ((NinjaWithKeyedWeapon)x).Weapon.Type == nameof(Longsword)); + ((NinjaWithKeyedWeapon)warrior).Storage.Should().NotBeNull().And.BeOfType(typeof(KeyedWeaponStorage)).And + .Match(x => ((KeyedWeaponStorage)x).Weapon.Type == nameof(Lance)); + } + + [Fact] + public void OptionalKeyedServiceCollectionExisting_CorrectServiceResolved() + { + var collection = new ServiceCollection(); + collection.Add(new ServiceDescriptor(typeof(IWarrior),"Samurai", typeof(Samurai), ServiceLifetime.Transient)); + collection.Add(new ServiceDescriptor(typeof(IWarrior), "Ninja1", new Ninja("test"))); + collection.Add(new ServiceDescriptor(typeof(IWarrior),"Ninja2", + (provider, key) => new Ninja("test:" + key.ToString()), ServiceLifetime.Transient)); + var kernel = CreateTestKernel(collection); + var provider = CreateServiceProvider(kernel); + + provider.GetKeyedService(typeof(IWarrior), "Samurai").Should().NotBeNull().And.BeOfType(typeof(Samurai)); + provider.GetKeyedService(typeof(IWarrior), "Ninja1").Should().NotBeNull().And.BeOfType(typeof(Ninja)). + And.Match(x => ((Ninja)x).Name == "test"); + var ninja2First = provider.GetKeyedService(typeof(IWarrior), "Ninja2"); + var ninja2Second = provider.GetKeyedService(typeof(IWarrior), "Ninja2"); + ninja2First.Should().NotBeNull().And.BeOfType(typeof(Ninja)). + And.Match(x => ((Ninja)x).Name == "test:Ninja2"); + ninja2Second.Should().NotBeNull().And.BeOfType(typeof(Ninja)). + And.Match(x => ((Ninja)x).Name == "test:Ninja2"); + ninja2First.Should().NotBeSameAs(ninja2Second); + } + + [Fact] + public void OptionalKeyedNinjectDirectBindingExisting_CorrectServiceResolved() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai")); + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja")); + var provider = CreateServiceProvider(kernel); + + provider.GetKeyedService(typeof(IWarrior), "Samurai").Should().NotBeNull().And.BeOfType(typeof(Samurai)); + provider.GetKeyedService(typeof(IWarrior), "Ninja").Should().NotBeNull().And.BeOfType(typeof(Ninja)); + } + + [Fact] + public void OptionalKeyedNonExisting_SingleServiceResolvedToNull() + { + var kernel = CreateTestKernel(); + var provider = CreateServiceProvider(kernel); + + provider.GetKeyedService(typeof(IWarrior), "Samurai").Should().BeNull(); + } + + [Fact] + public void OptionalExistingMultipleKeydServices_ResolvedQueriedAsList() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Warrior"));; + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Warrior")); + var provider = CreateServiceProvider(kernel); + + var result = provider.GetKeyedService(typeof(IList), "Warrior") as IEnumerable; + + result.Should().NotBeNull(); + var resultList = result.ToList(); + resultList.Should().HaveCount(2); + resultList.Should().Contain(x => x is Samurai); + resultList.Should().Contain(x => x is Ninja); + } + + [Fact] + public void OptionalExistingMultipleKeydServices_NotResolvedAsListNonKeyed() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai"));; + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja")); + var provider = CreateServiceProvider(kernel); + + var result = provider.GetService(typeof(IList)) as IEnumerable; + + result.Should().NotBeNull(); + var resultList = result.ToList(); + resultList.Should().HaveCount(0); + } + + [Fact] + public void ExistingMultipleServices_ResolvesNonKeyedToNull() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai"));; + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja")); + var provider = CreateServiceProvider(kernel); + + provider.GetService(typeof(IWarrior)).Should().BeNull(); + } + + [Fact] + public void RequiredKeyedServiceCollectionExisting_CorrectServiceResolved() + { + var collection = new ServiceCollection(); + collection.Add(new ServiceDescriptor(typeof(IWarrior),"Samurai", typeof(Samurai), ServiceLifetime.Transient)); + collection.Add(new ServiceDescriptor(typeof(IWarrior), "Ninja", new Ninja("test"))); + var kernel = CreateTestKernel(collection); + var provider = CreateServiceProvider(kernel); + + provider.GetRequiredKeyedService(typeof(IWarrior), "Ninja").Should().NotBeNull().And.BeOfType(typeof(Ninja)); + provider.GetRequiredKeyedService(typeof(IWarrior), "Samurai").Should().NotBeNull().And.BeOfType(typeof(Samurai)); + + } + + [Fact] + public void RequiredKeyedNinjectDirectBindingExisting_CorrectServiceResolved() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai")); + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja")); + var provider = CreateServiceProvider(kernel); + + provider.GetRequiredKeyedService(typeof(IWarrior), "Samurai").Should().NotBeNull().And.BeOfType(typeof(Samurai)); + provider.GetRequiredKeyedService(typeof(IWarrior), "Ninja").Should().NotBeNull().And.BeOfType(typeof(Ninja)); + } + + [Fact] + public void RequiredKeyedNonExisting_SingleServiceResolvedToException() + { + var kernel = CreateTestKernel(); + var provider = CreateServiceProvider(kernel); + + Action action = () => provider.GetRequiredKeyedService(typeof(IWarrior), "Samurai"); + action.Should().Throw().WithInnerException().WithMessage("*No matching bindings are available*"); + } + + [Fact] + public void RequiredExistingMultipleKeydServices_ResolvedQueriedAsList() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Warrior"));; + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Warrior")); + var provider = CreateServiceProvider(kernel); + + var result = provider.GetRequiredKeyedService(typeof(IList), "Warrior") as IEnumerable; + + result.Should().NotBeNull(); + var resultList = result.ToList(); + resultList.Should().HaveCount(2); + resultList.Should().Contain(x => x is Samurai); + resultList.Should().Contain(x => x is Ninja); + } + + [Fact] + public void RequiredExistingMultipleKeydServices_NotResolvedAsListNonKeyed() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai"));; + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja")); + var provider = CreateServiceProvider(kernel); + + var result = provider.GetRequiredService(typeof(IList)) as IEnumerable; + + result.Should().NotBeNull(); + var resultList = result.ToList(); + resultList.Should().HaveCount(0); + } + + [Fact] + public void ExistingMultipleServices_ResolvesNonKeyedToException() + { + var kernel = CreateTestKernel(); + kernel.Bind().To().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai"));; + kernel.Bind().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja")); + var provider = CreateServiceProvider(kernel); + + Action action = () => provider.GetRequiredService(typeof(IWarrior)); + action.Should().Throw().WithInnerException().WithMessage("*No matching bindings are available, and the type is not self-bindable.*"); + } + + private IServiceProvider CreateServiceProvider(AspNetCoreKernel kernel) + { + NinjectServiceProviderBuilder builder = CreateServiceProviderBuilder(kernel); + var provider = builder.Build(); + return provider; + } + + private NinjectServiceProviderBuilder CreateServiceProviderBuilder(AspNetCoreKernel kernel) + { + var collection = new ServiceCollection(); + var factory = new NinjectServiceProviderFactory(kernel); + var builder = factory.CreateBuilder(collection); + return builder; + } + + private AspNetCoreKernel CreateTestKernel(IServiceCollection collection = null) + { + var kernel = new AspNetCoreKernel(new NinjectSettings() { LoadExtensions = false }); + kernel.Load(typeof(AspNetCoreApplicationPlugin).Assembly); + if (collection != null) + { + new ServiceCollectionAdapter().Populate(kernel, collection); + } + + return kernel; + } + + } +#endif +} diff --git a/src/Ninject.Web.AspNetCore.Test/Unit/ServiceProviderTest.cs b/src/Ninject.Web.AspNetCore.Test/Unit/ServiceProviderTest.cs index a387765..e7b66fa 100644 --- a/src/Ninject.Web.AspNetCore.Test/Unit/ServiceProviderTest.cs +++ b/src/Ninject.Web.AspNetCore.Test/Unit/ServiceProviderTest.cs @@ -86,7 +86,7 @@ public void RequiredNonExistingSingleServiceResolvedToException() var provider = CreateServiceProvider(kernel); Action action = () => provider.GetRequiredService(typeof(IWarrior)); - action.Should().Throw().WithMessage("*No matching bindings are available*"); + action.Should().Throw().WithInnerException().WithMessage("*No matching bindings are available*"); } [Fact] @@ -115,7 +115,7 @@ public void RequiredExistingMultipleServicesResolvedToExceptionWhenNotQueriedAsL var provider = CreateServiceProvider(kernel); Action action = () => provider.GetRequiredService(typeof(IWarrior)); - action.Should().Throw().WithMessage("*More than one matching bindings are available*"); + action.Should().Throw().WithInnerException().WithMessage("*More than one matching bindings are available*"); } [Fact] diff --git a/src/Ninject.Web.AspNetCore/AspNetCoreKernel.cs b/src/Ninject.Web.AspNetCore/AspNetCoreKernel.cs index 08ba9cc..8a2e056 100644 --- a/src/Ninject.Web.AspNetCore/AspNetCoreKernel.cs +++ b/src/Ninject.Web.AspNetCore/AspNetCoreKernel.cs @@ -7,6 +7,10 @@ using Ninject.Planning.Bindings.Resolvers; using Ninject.Web.AspNetCore.Components; using System; +using System.Linq; +using Ninject.Planning.Strategies; +using Ninject.Web.AspNetCore.Parameters; +using Ninject.Web.AspNetCore.Planning; namespace Ninject.Web.AspNetCore { @@ -32,8 +36,11 @@ protected override Func SatifiesRequest(IRequest request) { return binding => { var latest = true; - if (request.IsUnique && request.Constraint == null) + if (request.IsUnique) { + // as we can't register constraints via microsoft.extensions.dependencyinjection, + // we always check for the latest binding + // Note that we have at least one constraint for the servicekey >= .NET 8.0 latest = binding.Metadata.Get(nameof(BindingIndex))?.IsLatest ?? true; } return binding.Matches(request) && request.Matches(binding) && latest; @@ -49,10 +56,16 @@ protected override void AddComponents() Components.Add(); Components.Remove(); Components.Add(); + Components.Remove(); + Components.Add(); Components.Add(); Components.Remove(); Components.Add(); + +#if NET8_0_OR_GREATER + Components.Add(); +#endif } public void DisableAutomaticSelfBinding() diff --git a/src/Ninject.Web.AspNetCore/BindingIndex.cs b/src/Ninject.Web.AspNetCore/BindingIndex.cs index f29a110..6afbc3e 100644 --- a/src/Ninject.Web.AspNetCore/BindingIndex.cs +++ b/src/Ninject.Web.AspNetCore/BindingIndex.cs @@ -1,11 +1,27 @@ using System; using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; namespace Ninject.Web.AspNetCore { public class BindingIndex { - private readonly IDictionary _bindingIndexMap = new Dictionary(); + + /// + /// Used as key for storing BindingIndex for unkeyed services. + /// + public sealed class UnkeyedIndexKey + { + private UnkeyedIndexKey() + { + } + + public static UnkeyedIndexKey Instance { get; } = new UnkeyedIndexKey(); + + public override string ToString() => nameof(UnkeyedIndexKey); + } + + private readonly IDictionary _bindingIndexMap = new Dictionary(); public int Count { get; private set; } @@ -13,20 +29,21 @@ public BindingIndex() { } - public Item Next(Type serviceType) + public Item Next(Type serviceType, object indexKey) { - - _bindingIndexMap.TryGetValue(serviceType, out var previous); + var serviceTypeKey = new ServiceTypeKey(serviceType, indexKey); + _bindingIndexMap.TryGetValue(serviceTypeKey, out var previous); - var next = new Item(this, serviceType, Count++, previous?.TypeIndex + 1 ?? 0); - _bindingIndexMap[serviceType] = next; + var next = new Item(this, serviceType, indexKey, Count++, previous?.TypeIndex + 1 ?? 0); + _bindingIndexMap[serviceTypeKey] = next; return next; } - private bool IsLatest(Type serviceType, Item item) + private bool IsLatest(Type serviceType, object registeredIndexKey, Item item) { - return _bindingIndexMap[serviceType] == item; + var match = _bindingIndexMap[new ServiceTypeKey(serviceType, registeredIndexKey)] == item; + return match; } public class Item @@ -36,16 +53,56 @@ public class Item public int TotalIndex { get; } public int TypeIndex { get; } - - public bool IsLatest => _root.IsLatest(_serviceType, this); + public object IndexKey { get; } + public int Precedence => _root.Count - TotalIndex; - public Item(BindingIndex root, Type serviceType, int totalIndex, int typeIndex) + public Item(BindingIndex root, Type serviceType, object indexKey, int totalIndex, int typeIndex) { _root = root; _serviceType = serviceType; TotalIndex = totalIndex; TypeIndex = typeIndex; + IndexKey = indexKey; + } + + public bool IsLatest => _root.IsLatest(_serviceType, IndexKey, this); + } + + /// + /// We have to to separate the precedence by servicekey. + /// This ensures that a binding with a different servicekey + /// can't override a binding with a non-matching servicekey + /// + public class ServiceTypeKey : IEquatable + { + public Type ServiceType { get; } + public object IndexKey { get; } + + public ServiceTypeKey(Type serviceType, object indexKey) + { + ServiceType = serviceType; + IndexKey = indexKey; + } + + public bool Equals(ServiceTypeKey other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(ServiceType, other.ServiceType) && Equals(IndexKey, other.IndexKey); + } + + public override bool Equals(object obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((ServiceTypeKey)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(ServiceType, IndexKey); } } } diff --git a/src/Ninject.Web.AspNetCore/Components/ConstrainedGenericBindingResolver.cs b/src/Ninject.Web.AspNetCore/Components/ConstrainedGenericBindingResolver.cs index 79a0c7a..98afef9 100644 --- a/src/Ninject.Web.AspNetCore/Components/ConstrainedGenericBindingResolver.cs +++ b/src/Ninject.Web.AspNetCore/Components/ConstrainedGenericBindingResolver.cs @@ -28,9 +28,9 @@ public IEnumerable Resolve(Multimap bindings, Type ser // that the next request for the same service type doesn't need to resolve this again. return bindings[service.GetGenericTypeDefinition()].Where(binding => { // If the binding has a ServiceDescriptor in its metadata, then we - if (binding.Target == BindingTarget.Type && binding.Metadata.Has(nameof(ServiceDescriptor))) + if (binding.Target == BindingTarget.Type && binding.Metadata.Has(nameof(IDescriptorAdapter))) { - return SatisfiesGenericTypeConstraints(service, binding.Metadata.Get(nameof(ServiceDescriptor)).ImplementationType); + return SatisfiesGenericTypeConstraints(service, binding.Metadata.Get(nameof(IDescriptorAdapter)).ImplementationType); } // ... otherwise we default to the OpenGenericBindingResolver which returns _all_ the bindings without regard for their generic constraints diff --git a/src/Ninject.Web.AspNetCore/Components/ConstructorReflectionStrategyWithKeyedSupport.cs b/src/Ninject.Web.AspNetCore/Components/ConstructorReflectionStrategyWithKeyedSupport.cs new file mode 100644 index 0000000..15172e8 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/Components/ConstructorReflectionStrategyWithKeyedSupport.cs @@ -0,0 +1,69 @@ +using System; +using System.Reflection; +using Ninject.Components; +using Ninject.Infrastructure.Language; +using Ninject.Injection; +using Ninject.Planning; +using Ninject.Planning.Directives; +using Ninject.Planning.Strategies; +using Ninject.Selection; +using Ninject.Web.AspNetCore.Planning; + +namespace Ninject.Web.AspNetCore.Components +{ + /// + /// Adds a directive to plans indicating which constructor should be injected during activation. + /// Need a custom one to support FromKeyedServices attribute, which doesn't inherit from ConstraintAttribute + /// + public class ConstructorReflectionStrategyWithKeyedSupport : NinjectComponent, + IPlanningStrategy + { + /// + /// Initializes a new instance of the class. + /// + /// The selector component. + /// The injector factory component. + public ConstructorReflectionStrategyWithKeyedSupport(ISelector selector, IInjectorFactory injectorFactory) + { + Selector = selector; + InjectorFactory = injectorFactory; + } + + /// + /// Gets the selector component. + /// + public ISelector Selector { get; } + + /// + /// Gets or sets the injector factory component. + /// + public IInjectorFactory InjectorFactory { get; } + + /// + /// Adds a to the plan for the constructor + /// that should be injected. + /// + /// The plan that is being generated. + public void Execute(IPlan plan) + { + var constructors = Selector.SelectConstructorsForInjection(plan.Type); + if (constructors == null) + { + return; + } + + foreach (ConstructorInfo constructor in constructors) + { + var hasInjectAttribute = constructor.HasAttribute(Settings.InjectAttribute); + var hasObsoleteAttribute = constructor.HasAttribute(typeof(ObsoleteAttribute)); + var directive = new ConstructorInjectionDirectiveWithKeyedSupport(constructor, InjectorFactory.Create(constructor)) + { + HasInjectAttribute = hasInjectAttribute, + HasObsoleteAttribute = hasObsoleteAttribute, + }; + + plan.Add(directive); + } + } + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/DefaultDescriptorAdapter.cs b/src/Ninject.Web.AspNetCore/DefaultDescriptorAdapter.cs new file mode 100644 index 0000000..9928d34 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/DefaultDescriptorAdapter.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Ninject.Activation; + +namespace Ninject.Web.AspNetCore +{ + /// + /// This ServiceDescriptorAdapter is used when ServiceDescriptor.IsKeyedService == false + /// This was always the case before .NET 8.0 + /// + public class DefaultDescriptorAdapter : IDescriptorAdapter + { + private ServiceDescriptor _descriptor; + + public DefaultDescriptorAdapter(ServiceDescriptor descriptor) + { + _descriptor = descriptor; + } + + public Type ImplementationType => _descriptor.ImplementationType; + public object ImplementationInstance => _descriptor.ImplementationInstance; + public bool UseServiceFactory => _descriptor.ImplementationFactory != null; + public object InstantiateFromServiceFactory(IServiceProvider provider, IContext context) + { + return _descriptor.ImplementationFactory(provider); + } + public ServiceLifetime Lifetime => _descriptor.Lifetime; + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/IDescriptorAdapter.cs b/src/Ninject.Web.AspNetCore/IDescriptorAdapter.cs new file mode 100644 index 0000000..a407dc1 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/IDescriptorAdapter.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Ninject.Activation; + +namespace Ninject.Web.AspNetCore +{ + /// + /// This interface allows to handle the differences between keyed and non keyed implementation instruction + /// on ServiceDescriptors + /// + public interface IDescriptorAdapter + { + /// + /// Returns the type to instantiate if instantiation should be done by type. + /// + Type ImplementationType { get; } + + /// + /// Returns the instance if a specific instance is configured on the descriptor + /// + object ImplementationInstance { get; } + + /// + /// Returns true, if a service factory is configured on the descriptor + /// + bool UseServiceFactory { get; } + + /// + /// If UseServiceFactory returns true, use this method to instantiate via factory. + /// + object InstantiateFromServiceFactory(IServiceProvider provider, IContext context); + + /// + /// The lifetime coonfigured for the service descriptor + /// + ServiceLifetime Lifetime { get; } + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/KeyedDescriptorAdapter.cs b/src/Ninject.Web.AspNetCore/KeyedDescriptorAdapter.cs new file mode 100644 index 0000000..3d98976 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/KeyedDescriptorAdapter.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Ninject.Activation; +using Ninject.Web.AspNetCore.Parameters; + +namespace Ninject.Web.AspNetCore +{ +#if NET8_0_OR_GREATER + /// + /// This ServiceDescriptorAdapter is used when ServiceDescriptor.IsKeyedService == true + /// + public class KeyedDescriptorAdapter : IDescriptorAdapter + { + + private ServiceDescriptor _descriptor; + + public KeyedDescriptorAdapter(ServiceDescriptor descriptor) + { + _descriptor = descriptor; + } + + public Type ImplementationType => _descriptor.KeyedImplementationType; + public object ImplementationInstance => _descriptor.KeyedImplementationInstance; + public bool UseServiceFactory => _descriptor.KeyedImplementationFactory != null; + public object InstantiateFromServiceFactory(IServiceProvider provider, IContext context) + { + object serviceKey = _descriptor.ServiceKey; + var keyParameter = context.Parameters.LastOrDefault(x => x is ServiceKeyParameter) as ServiceKeyParameter; + if (keyParameter != null) + { + serviceKey = keyParameter.ServiceKey; + } + return _descriptor.KeyedImplementationFactory(provider, serviceKey); + } + + public ServiceLifetime Lifetime => _descriptor.Lifetime; + } +#endif +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/NinjectServiceProvider.cs b/src/Ninject.Web.AspNetCore/NinjectServiceProvider.cs index 06b0925..a4bdf21 100644 --- a/src/Ninject.Web.AspNetCore/NinjectServiceProvider.cs +++ b/src/Ninject.Web.AspNetCore/NinjectServiceProvider.cs @@ -1,6 +1,15 @@ using Microsoft.Extensions.DependencyInjection; using Ninject.Syntax; using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Ninject.Parameters; +using Ninject.Planning.Bindings; +using Ninject.Web.AspNetCore.Parameters; +using Ninject.Web.AspNetCore.Planning; +using Ninject.Web.AspNetCore.RequestActivation; namespace Ninject.Web.AspNetCore { @@ -19,7 +28,11 @@ namespace Ninject.Web.AspNetCore /// we pass an constructor argument when creating the root service provider. /// public class NinjectServiceProvider : IServiceProvider, ISupportRequiredService, IDisposable +#if NET8_0_OR_GREATER + , IKeyedServiceProvider +#endif { + private static readonly MethodInfo EnumerableCastMethod = typeof(Enumerable).GetMethod(nameof(Enumerable.Cast)); private readonly IResolutionRoot _resolutionRoot; private readonly IServiceScope _scope; @@ -31,13 +44,41 @@ public NinjectServiceProvider(IResolutionRoot resolutionRoot, IServiceScope scop public object GetRequiredService(Type serviceType) { - var result = _resolutionRoot.Get(serviceType); - return result; + if (!IsListType(serviceType, out var elementType)) + { + try + { + return _resolutionRoot.Get(serviceType, metadata => !metadata.HasServiceKeyMetadata()); + } + catch (ActivationException ex) + { + throw new InvalidOperationException($"Can't resolve service of Type {serviceType}", ex); + } + } + else + { + // Ninject is not evaluating metadata constraint when resolving a IEnumerable, see KernelBase.UpdateRequest + // Therefore, need to implement a workaround to not instantiate here bindings with servicekey + return ConvertToTypedEnumerable(elementType, + _resolutionRoot.GetAll(elementType, metadata => !metadata.HasServiceKeyMetadata())); + } } public object GetService(Type serviceType) { - var result = _resolutionRoot.TryGet(serviceType); + object result; + if (!IsListType(serviceType, out var elementType)) + { + result = _resolutionRoot.TryGet(serviceType, metadata => !metadata.HasServiceKeyMetadata()); + } + else + { + // Ninject is not evaluating metadata constraint when resolving a IEnumerable, see KernelBase.UpdateRequest + // Therefore, need to implement a workaround to not instantiate here bindings with servicekey + result = ConvertToTypedEnumerable(elementType, + _resolutionRoot.GetAll(elementType, metadata => !metadata.HasServiceKeyMetadata())); + } + return result; } @@ -45,5 +86,123 @@ public void Dispose() { _scope?.Dispose(); } + +#if NET8_0_OR_GREATER + public object GetKeyedService(Type serviceType, object serviceKey) + { + if (serviceKey == null) + { + // serviceKey = null means unkeyed + return GetService(serviceType); + } + + object result; + if (!IsListType(serviceType, out var elementType)) + { + EnsureNotAnyKey(serviceKey, serviceType); + return ResolveKeyedService(serviceType, serviceKey, true, true); + } + else + { + // Ninject is not evaluating metadata constraint when resolving a IEnumerable, see KernelBase.UpdateRequest + // Therefore, need to implement a workaround to not instantiate here bindings with a different servicekey value + result = ConvertToTypedEnumerable(elementType, + ResolveKeyedService>(elementType, serviceKey, false, true)); + } + + return result; + } + + public object GetRequiredKeyedService(Type serviceType, object serviceKey) + { + if (serviceKey == null) + { + // serviceKey = null means unkeyed + return GetRequiredService(serviceType); + } + + if (!IsListType(serviceType, out var elementType)) + { + EnsureNotAnyKey(serviceKey, serviceType); + try + { + return ResolveKeyedService(serviceType, serviceKey, true, false); + } + catch (ActivationException ex) + { + throw new InvalidOperationException($"Can't resolve service of Type {serviceType} with service key {serviceKey}", ex); + } + } + else + { + // Ninject is not evaluating metadata constraint when resolving a IEnumerable, see KernelBase.UpdateRequest + // Therefore, need to implement a workaround to not instantiate here bindings with a different servicekey value + return ConvertToTypedEnumerable(elementType, + ResolveKeyedService>(elementType, serviceKey, false, true)); + } + } + + private T ResolveKeyedService(Type serviceType, object serviceKey, bool isUnique, bool isOptional) where T : class + { + var standardRequest = _resolutionRoot.CreateRequest(serviceType, metadata => metadata.DoesMetadataMatchServiceKey(serviceKey), Array.Empty(), isOptional, isUnique); + var keyedRequest = standardRequest.ToKeyedRequest(serviceKey); + var result = _resolutionRoot.Resolve(keyedRequest); + if (isUnique) + { + return (result as object[]).FirstOrDefault() as T; + } + else + { + return result as T; + } + } + + private void EnsureNotAnyKey(object serviceKey, Type serviceType) + { + if (serviceKey == KeyedService.AnyKey) + { + throw new InvalidOperationException($"Not allowed to resolve a service {serviceType} with the KeyedService.AnyKey. " + + $"That's only supported when resolving collections of services."); + } + } + +#endif + + /// + /// This method extracts the elementtype in the same way as Ninject does + /// in KernelBase.Resolve + /// + private static bool IsListType(Type type, out Type elementType) + { + if (type.IsArray) + { + elementType = type.GetElementType(); + return true; + } + + if (type.IsGenericType) + { + Type genericTypeDefinition = type.GetGenericTypeDefinition(); + if (genericTypeDefinition == typeof(IEnumerable<>) || + genericTypeDefinition == typeof(List<>) || genericTypeDefinition == typeof(IList<>) || + genericTypeDefinition == typeof(ICollection<>)) + { + elementType = type.GenericTypeArguments[0]; + return true; + } + } + + elementType = null; + return false; + } + + private static object ConvertToTypedEnumerable(Type elementType, IEnumerable objectList) + { + var castMethod = EnumerableCastMethod.MakeGenericMethod(elementType); + var result = (IEnumerable)castMethod.Invoke(null, new object[] { objectList }); + + return result; + } + } } diff --git a/src/Ninject.Web.AspNetCore/NinjectServiceProviderBuilder.cs b/src/Ninject.Web.AspNetCore/NinjectServiceProviderBuilder.cs index f446367..65d7104 100644 --- a/src/Ninject.Web.AspNetCore/NinjectServiceProviderBuilder.cs +++ b/src/Ninject.Web.AspNetCore/NinjectServiceProviderBuilder.cs @@ -28,7 +28,7 @@ public IServiceProvider Build() var scopeProvider = context.GetServiceProviderScopeParameter()?.SourceServiceProvider; if (scopeProvider != null) { - var descriptor = context.Request.ParentContext?.Binding.Metadata.Get(nameof(ServiceDescriptor)); + var descriptor = context.Request.ParentContext?.Binding.Metadata.Get(nameof(IDescriptorAdapter)); if (descriptor == null || descriptor.Lifetime != ServiceLifetime.Singleton) { return scopeProvider; @@ -40,6 +40,9 @@ public IServiceProvider Build() #if NET6_0_OR_GREATER _kernel.Bind().To().InSingletonScope(); #endif +#if NET8_0_OR_GREATER + _kernel.Bind().To().InSingletonScope(); +#endif var adapter = new ServiceCollectionAdapter(); adapter.Populate(_kernel, _services); diff --git a/src/Ninject.Web.AspNetCore/NinjectServiceProviderIsService.cs b/src/Ninject.Web.AspNetCore/NinjectServiceProviderIsService.cs index 400b289..32f9c2f 100644 --- a/src/Ninject.Web.AspNetCore/NinjectServiceProviderIsService.cs +++ b/src/Ninject.Web.AspNetCore/NinjectServiceProviderIsService.cs @@ -1,11 +1,15 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; +using Ninject.Web.AspNetCore.Planning; namespace Ninject.Web.AspNetCore { #if NET6_0_OR_GREATER public class NinjectServiceProviderIsService : IServiceProviderIsService +#if NET8_0_OR_GREATER + , IServiceProviderIsKeyedService +#endif { private readonly IKernel _kernel; @@ -31,6 +35,26 @@ public bool IsService(Type serviceType) return _kernel.CanResolve(serviceType); } +#if NET8_0_OR_GREATER + public bool IsKeyedService(Type serviceType, object serviceKey) + { + // IsService should only return true if the type can actually be resolved to a service + // and open generic types cannot. Except for IEnumerable which should return true + // in ANY case (see DependencyInjectionSpecificationTests.IEnumerableWithIsServiceAlwaysReturnsTrue) + if (serviceType.IsGenericTypeDefinition) + { + return false; + } + + if (serviceType.IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return true; + } + + return _kernel.CanResolve(serviceType, metadata => + metadata.DoesMetadataMatchServiceKey(serviceKey)); + } +#endif } #endif } diff --git a/src/Ninject.Web.AspNetCore/Parameters/ServiceKeyParameter.cs b/src/Ninject.Web.AspNetCore/Parameters/ServiceKeyParameter.cs new file mode 100644 index 0000000..4a5d0d7 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/Parameters/ServiceKeyParameter.cs @@ -0,0 +1,15 @@ +using Ninject.Parameters; + + +namespace Ninject.Web.AspNetCore.Parameters +{ + public class ServiceKeyParameter : Parameter + { + public ServiceKeyParameter(object value) : base(nameof(ServiceKeyParameter), value, true) + { + ServiceKey = value; + } + + public object ServiceKey { get; } + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/Planning/ConstructorInjectionDirectiveWithKeyedSupport.cs b/src/Ninject.Web.AspNetCore/Planning/ConstructorInjectionDirectiveWithKeyedSupport.cs new file mode 100644 index 0000000..d962f91 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/Planning/ConstructorInjectionDirectiveWithKeyedSupport.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Ninject.Injection; +using Ninject.Planning.Directives; +using Ninject.Planning.Targets; + +namespace Ninject.Web.AspNetCore.Planning +{ + public class ConstructorInjectionDirectiveWithKeyedSupport : ConstructorInjectionDirective + { + public ConstructorInjectionDirectiveWithKeyedSupport(ConstructorInfo constructor, ConstructorInjector injector) : base(constructor, injector) + { + } + + protected override ITarget[] CreateTargetsFromParameters(ConstructorInfo method) + { + return method.GetParameters(). + Select((Func) (parameter => new ParameterTargetWithKeyedSupport(method, parameter))). + ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/Planning/KeyedServiceAnyKeyResolver.cs b/src/Ninject.Web.AspNetCore/Planning/KeyedServiceAnyKeyResolver.cs new file mode 100644 index 0000000..d6d70ed --- /dev/null +++ b/src/Ninject.Web.AspNetCore/Planning/KeyedServiceAnyKeyResolver.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Ninject.Activation; +using Ninject.Components; +using Ninject.Infrastructure; +using Ninject.Planning.Bindings; +using Ninject.Planning.Bindings.Resolvers; +using Ninject.Web.AspNetCore.RequestActivation; + +namespace Ninject.Web.AspNetCore.Planning +{ +#if NET8_0_OR_GREATER + /// + /// This class is used to handle keyed services registrations with service key equal to + /// KeyedService.AnyKey. + /// If such a binding is resolved unique with a key different to KeyedService.AnyKey, + /// we need to dynamically add a new matching binding with this service key. + /// The missing binding resolver adds a binding with the metadata for this service key, so that + /// it can be resolved in this resolution and in potential next resolutions as well. + /// + public class KeyedServiceAnyKeyResolver : NinjectComponent, IMissingBindingResolver + { + public IEnumerable Resolve(Multimap bindings, IRequest request) + { + // we resolve here request with a specific service key, but only having a binding with anykey. + // this ensures that we e.g. can have a singleton binding with anykey, but instantiate one singleton + // per servicekey. + var keyedRequest = request as KeyedRequest; + if (keyedRequest != null && keyedRequest.ServiceKey != null && keyedRequest.ServiceKey != KeyedService.AnyKey + && keyedRequest.IsUnique) + { + var service = request.Service; + var matchingBindings = bindings.Where(x => x.Key == service); + if (!matchingBindings.Any()) + { + return Array.Empty(); + } + + IBinding matchingAnyBinding = null; + foreach (var bindingGroup in matchingBindings) + { + foreach (var binding in bindingGroup.Value) + { + if (binding.Metadata.HasServiceKeyMetadata() && Object.Equals(binding.Metadata.GetServiceKey(), KeyedService.AnyKey) + && (binding.Metadata.Get(nameof(BindingIndex))?.IsLatest ?? true) + ) + { + matchingAnyBinding = binding; + break; + } + } + } + + if (matchingAnyBinding == null) + { + return Array.Empty(); + } + + var resultBinding = new Binding(service) + { + IsImplicit = true, + ProviderCallback = matchingAnyBinding.ProviderCallback, + ScopeCallback = matchingAnyBinding.ScopeCallback, + Target = matchingAnyBinding.Target + }; + var bindingIndex = new BindingIndex(); + resultBinding.Metadata.Set(nameof(BindingIndex), bindingIndex.Next(service, keyedRequest.ServiceKey)); + resultBinding.Metadata.Set(nameof(ServiceKey), new ServiceKey(keyedRequest.ServiceKey)); + resultBinding.Metadata.Set(nameof(IDescriptorAdapter), matchingAnyBinding.Metadata.Get(nameof(IDescriptorAdapter))); + + return new Binding[1] + { + resultBinding + }; + } + + return Array.Empty(); + } + } +#endif +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/Planning/KeyedServicesMetaDataExtensions.cs b/src/Ninject.Web.AspNetCore/Planning/KeyedServicesMetaDataExtensions.cs new file mode 100644 index 0000000..dfed78d --- /dev/null +++ b/src/Ninject.Web.AspNetCore/Planning/KeyedServicesMetaDataExtensions.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Ninject.Planning.Bindings; + +namespace Ninject.Web.AspNetCore.Planning +{ + /// + /// Extensions to handle ServiceKey. + /// Only really relevant for >= .NET 8.0, as only there Microsoft DI supports keyed services. + /// + public static class KeyedServicesMetaDataExtensions + { + +#if NET8_0_OR_GREATER + internal static bool DoesMetadataMatchServiceKey(this IBindingMetadata metadata, object serviceKey) + { + if (serviceKey == KeyedService.AnyKey) + { + // if the service is registered with KeyedService.AnyKey, it must not be returned when querying with AnyKey + // see CombinationalRegistration compliancetest + return HasServiceKeyMetadata(metadata) && !Object.Equals(metadata.GetServiceKey(), KeyedService.AnyKey); + } + + return Object.Equals(metadata.GetServiceKey(), serviceKey); + // if we query with a key different to KeyedService.AnyKey but registired with AnyKey, we have to instantiate it in the end + // but we do this with a missingbinding resolver, the KeyedServiceAnyKeyResolver. But only if we resolve a unique instance + // see ResolveKeyedServiceSingletonInstanceWithAnyKey compliancetest. + } + + internal static object GetServiceKey(this IBindingMetadata metadata) + { + return metadata.Get(nameof(ServiceKey))?.Key; + } +#endif + internal static bool HasServiceKeyMetadata(this IBindingMetadata metadata) + { + return metadata.Has(nameof(ServiceKey)); + } + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/Planning/ParameterTargetWithKeyedSupport.cs b/src/Ninject.Web.AspNetCore/Planning/ParameterTargetWithKeyedSupport.cs new file mode 100644 index 0000000..3b025cd --- /dev/null +++ b/src/Ninject.Web.AspNetCore/Planning/ParameterTargetWithKeyedSupport.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Ninject.Activation; +using Ninject.Parameters; +using Ninject.Planning.Bindings; +using Ninject.Planning.Targets; +using Ninject.Web.AspNetCore.Parameters; +using Ninject.Web.AspNetCore.RequestActivation; + +namespace Ninject.Web.AspNetCore.Planning +{ + public class ParameterTargetWithKeyedSupport : ParameterTarget, ITarget + { +#if NET8_0_OR_GREATER + private readonly Lazy _serviceKeyAttribute; + private readonly Lazy _fromKeyedServicesAttribute; +#endif + + public ParameterTargetWithKeyedSupport(MethodBase method, ParameterInfo site) : base(method, site) + { +#if NET8_0_OR_GREATER + _serviceKeyAttribute = new Lazy(ReadServiceKeyAttribute); + _fromKeyedServicesAttribute = new Lazy(ReadFromKeyedServicesAttribute); +#endif + } + + public override bool HasDefaultValue + { + get + { + var result = this.Site.HasDefaultValue; +#if NET8_0_OR_GREATER + // ensure that constructor scorer knows that we have a default value for parameters decorated with ServiceKey + // as the DefaultValueBindingResolver is only a MissingBindingResolver, the + // ParameterTargetWithKeyedSupport.ResolveWithin method already + // provided a default value before any Ninject resolution for the value happens. + result = result || _serviceKeyAttribute.Value != null; +#endif + return result; + } + } + + /// + /// MethodInjectionStrategy.GetMethodArguments calls ITarget.ResolveWithin. + /// As we can't override the base implementation as it is not virtual, the + /// explicit interface implementation helps to still delegate the resolution to here. + /// + object ITarget.ResolveWithin(IContext parent) + { +#if NET8_0_OR_GREATER + if (_serviceKeyAttribute.Value != null) + { + return ResolveServiceKeyValue(parent); + } + + if (_fromKeyedServicesAttribute.Value != null) + { + return ResolveFromKeyedService(parent, _fromKeyedServicesAttribute.Value); + } +#endif + return base.ResolveWithin(parent); + } + +#if NET8_0_OR_GREATER + private ServiceKeyAttribute ReadServiceKeyAttribute() + { + var serviceKeyAttributes = GetCustomAttributes(typeof(ServiceKeyAttribute), true) as ServiceKeyAttribute[]; + return serviceKeyAttributes?.Length > 0 ? serviceKeyAttributes[0] : null; + } + + private FromKeyedServicesAttribute ReadFromKeyedServicesAttribute() + { + var keyedattributes = GetCustomAttributes(typeof (FromKeyedServicesAttribute), true) as FromKeyedServicesAttribute[]; + return keyedattributes?.Length > 0 ? keyedattributes[0] : null; + } + + private object ResolveFromKeyedService(IContext parent, FromKeyedServicesAttribute keyedattribute) + { + var fromKeyedServiceValue = DeterimeFromKeyedServiceValue(keyedattribute, parent.Parameters); + var additionalConstraint = fromKeyedServiceValue != null + ? metadata => metadata.DoesMetadataMatchServiceKey(fromKeyedServiceValue) + : (Func)null; + var child = parent.Request.CreateKeyedChildRequest(Type, fromKeyedServiceValue, parent, this, + additionalConstraint); + child.IsUnique = true; + child.IsOptional = false; // constructor arguments marked with FromKeyedServices must always resolve, otherwise an InvalidOperationException is expected. + try + { + return parent.Kernel.Resolve(child).SingleOrDefault(); + } + catch (ActivationException ex) + { + if (Site.HasDefaultValue) + { + // in case we have a default value for the constructor parameter, we don't throw but use the default instead. + return Site.DefaultValue; + } + throw new InvalidOperationException( + $"Can't resolve keyed service of Type {Type} with key {fromKeyedServiceValue}", ex); + } + } + + private object DeterimeFromKeyedServiceValue( + FromKeyedServicesAttribute keyedattribute, ICollection parameters) + { +#if NET10_0_OR_GREATER + if (keyedattribute.LookupMode == ServiceKeyLookupMode.NullKey) + { + // means no constraint, resolve normally. + return null; + } + if (keyedattribute.LookupMode == ServiceKeyLookupMode.InheritKey) + { + var serviceKeyParam = parameters.LastOrDefault(x => x is ServiceKeyParameter) as ServiceKeyParameter; + return serviceKeyParam?.ServiceKey; + } +#endif + return keyedattribute.Key; + } + + private object ResolveServiceKeyValue(IContext parent) + { + var result = parent.Binding.Metadata.GetServiceKey(); + var serviceKeyParameter = parent.Parameters.LastOrDefault(x => x is ServiceKeyParameter) as ServiceKeyParameter; + if (serviceKeyParameter != null) + { + result = serviceKeyParameter.ServiceKey; + } + + var asConvertible = result as IConvertible; + if (asConvertible != null) + { + try + { + result = Convert.ChangeType(asConvertible, this.Type); + } + catch (InvalidCastException) + { + // we have to throw and InvalidOperationException in this case, a InvalidCastException + // is not passing the tests + throw new InvalidOperationException("Cannot convert " + asConvertible + " to " + this.Type); + } + } + + if (result != null && !this.Type.IsAssignableFrom(result.GetType())) + { + throw new InvalidOperationException("Cannot convert " + result + " to " + this.Type); + } + + return result; + } +#endif + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/RequestActivation/KeyedRequest.cs b/src/Ninject.Web.AspNetCore/RequestActivation/KeyedRequest.cs new file mode 100644 index 0000000..9624ca6 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/RequestActivation/KeyedRequest.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ninject.Activation; +using Ninject.Infrastructure.Introspection; +using Ninject.Parameters; +using Ninject.Planning.Bindings; +using Ninject.Planning.Targets; +using Ninject.Web.AspNetCore.Parameters; + +namespace Ninject.Web.AspNetCore.RequestActivation +{ + /// + /// Use a specific request class for keyed requests so that we can get the ServiceKey in the class. + /// Additionally, handle the ServiceKey parameter as well as allow to add additional constraints during resolution required for + /// FromKeyedServices attribute handling. + /// + public class KeyedRequest : IRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The service that was requested. + /// The constraint that will be applied to filter the bindings used for the request. + /// The parameters that affect the resolution. + /// The scope callback, if an external scope was specified. + /// if the request is optional; otherwise, . + /// if the request should return a unique result; otherwise, . + /// is . + /// is . + public KeyedRequest(Type service, object serviceKey, Func constraint, IReadOnlyList parameters, Func scopeCallback, bool isOptional, bool isUnique) + { + Service = service; + Constraint = constraint; + Parameters = new List(parameters); + Parameters.Add(new ServiceKeyParameter(serviceKey)); + ScopeCallback = scopeCallback; + ActiveBindings = new Stack(); + Depth = 0; + IsOptional = isOptional; + IsUnique = isUnique; + ServiceKey = serviceKey; + } + + /// + /// Initializes a new instance of the class. + /// + /// The parent context. + /// The service that was requested. + /// The target that will receive the injection. + /// The scope callback, if an external scope was specified. + public KeyedRequest(IContext parentContext, Type service, object serviceKey, ITarget target, Func scopeCallback, Func additionalConstraint = null) + { + ParentContext = parentContext; + ParentRequest = parentContext.Request; + Service = service; + Target = target; + Constraint = additionalConstraint == null + ? target.Constraint + : metadata => (target.Constraint?.Invoke(metadata) ?? true) && additionalConstraint(metadata); + IsOptional = target.IsOptional; + Parameters = new List(parentContext.Parameters.Where(x => x.ShouldInherit)); + var parametersToRemove = Parameters.Where(x => x is ServiceKeyParameter).ToList(); + foreach (var parameter in parametersToRemove) + { + Parameters.Remove(parameter); + } + Parameters.Add(new ServiceKeyParameter(serviceKey)); + ScopeCallback = scopeCallback; + ActiveBindings = new Stack(this.ParentRequest.ActiveBindings); + Depth = ParentRequest.Depth + 1; + ServiceKey = serviceKey; + } + + /// + /// Gets the service that was requested. + /// + public Type Service { get; private set; } + + /// + /// The specific servicekey to resovle this request for. + /// + public object ServiceKey { get; private set; } + + /// + /// Gets the parent request. + /// + public IRequest ParentRequest { get; private set; } + + /// + /// Gets the parent context. + /// + public IContext ParentContext { get; private set; } + + /// + /// Gets the target that will receive the injection, if any. + /// + public ITarget Target { get; private set; } + + /// + /// Gets the constraint that will be applied to filter the bindings used for the request. + /// + public Func Constraint { get; private set; } + + /// + /// Gets the parameters that affect the resolution. + /// + public ICollection Parameters { get; private set; } + + /// + /// Gets the stack of bindings which have been activated by either this request or its ancestors. + /// + public Stack ActiveBindings { get; private set; } + + /// + /// Gets the recursive depth at which this request occurs. + /// + public int Depth { get; private set; } + + /// + /// Gets or sets a value indicating whether the request is optional. + /// + public bool IsOptional { get; set; } + + /// + /// Gets or sets a value indicating whether the request is for a single service. + /// + public bool IsUnique { get; set; } + + /// + /// Gets or sets a value indicating whether the request should force to return a unique value even if the request is optional. + /// If this value is set true the request will throw an ActivationException if there are multiple satisfying bindings rather + /// than returning null for the request is optional. For none optional requests this parameter does not change anything. + /// + public bool ForceUnique { get; set; } + + /// + /// Gets the callback that resolves the scope for the request, if an external scope was provided. + /// + public Func ScopeCallback { get; private set; } + + /// + /// Determines whether the specified binding satisfies the constraints defined on this request. + /// + /// The binding. + /// + /// if the binding satisfies the constraints; otherwise, . + /// + public bool Matches(IBinding binding) + { + return this.Constraint == null || this.Constraint(binding.Metadata); + } + + /// + /// Gets the scope if one was specified in the request. + /// + /// + /// The object that acts as the scope. + /// + public object GetScope() + { + return this.ScopeCallback == null ? null : this.ScopeCallback(); + } + + /// + /// Creates a child request. + /// + /// The service that is being requested. + /// The context in which the request was made. + /// The target that will receive the injection. + /// + /// The child request. + /// + public IRequest CreateChild(Type service, IContext parentContext, ITarget target) + { + return new KeyedRequest(parentContext, service, null, target, this.ScopeCallback); + } + + /// + /// Formats this object into a meaningful string representation. + /// + /// + /// The request formatted as string. + /// + public override string ToString() + { + return this.Format(); + } + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/RequestActivation/KeyedRequestExtensions.cs b/src/Ninject.Web.AspNetCore/RequestActivation/KeyedRequestExtensions.cs new file mode 100644 index 0000000..d533e4f --- /dev/null +++ b/src/Ninject.Web.AspNetCore/RequestActivation/KeyedRequestExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Ninject.Activation; +using Ninject.Planning.Bindings; +using Ninject.Planning.Targets; + +namespace Ninject.Web.AspNetCore.RequestActivation; + +public static class KeyedRequestExtensions +{ + public static IRequest CreateKeyedChildRequest(this IRequest parentRequest, Type service, object serviceKey, + IContext parentContext, ITarget target, Func additionalConstraint = null) + { + return new KeyedRequest(parentContext, service, serviceKey, target, parentRequest.GetScope, additionalConstraint); + } + + public static IRequest ToKeyedRequest(this IRequest request, object serviceKey) + { + return new KeyedRequest(request.Service, serviceKey, request.Constraint, request.Parameters.ToList(), + request.GetScope, request.IsOptional, request.IsUnique); + } +} \ No newline at end of file diff --git a/src/Ninject.Web.AspNetCore/ServiceCollectionAdapter.cs b/src/Ninject.Web.AspNetCore/ServiceCollectionAdapter.cs index 06ff347..908b89a 100644 --- a/src/Ninject.Web.AspNetCore/ServiceCollectionAdapter.cs +++ b/src/Ninject.Web.AspNetCore/ServiceCollectionAdapter.cs @@ -54,13 +54,47 @@ private IBindingWithOrOnSyntax ConfigureImplementationAndLifecycle( IBindingToSyntax bindingToSyntax, ServiceDescriptor descriptor, BindingIndex bindingIndex) where T : class + { + IDescriptorAdapter adapter; +#if NET8_0_OR_GREATER + if (descriptor.IsKeyedService) + { + adapter = new KeyedDescriptorAdapter(descriptor); + } +#endif +#if NET8_0_OR_GREATER + else + { +#endif + adapter = new DefaultDescriptorAdapter(descriptor); +#if NET8_0_OR_GREATER + } +#endif + + var resultWithMetadata = ConfigureImplementationAndLifecycleWithAdapter(bindingToSyntax, adapter) + .WithMetadata(nameof(IDescriptorAdapter), adapter); + + object indexKey = BindingIndex.UnkeyedIndexKey.Instance; +#if NET8_0_OR_GREATER + if (descriptor.IsKeyedService) + { + resultWithMetadata = resultWithMetadata.WithMetadata(nameof(ServiceKey), new ServiceKey(descriptor.ServiceKey)); + indexKey = descriptor.ServiceKey; + } +#endif + resultWithMetadata = resultWithMetadata.WithMetadata(nameof(BindingIndex), bindingIndex.Next(descriptor.ServiceType, indexKey)); + return resultWithMetadata; + } + + private IBindingNamedWithOrOnSyntax ConfigureImplementationAndLifecycleWithAdapter(IBindingToSyntax bindingToSyntax, + IDescriptorAdapter adapter) where T : class { IBindingNamedWithOrOnSyntax result; - if (descriptor.ImplementationType != null) + if (adapter.ImplementationType != null) { - result = ConfigureLifecycle(bindingToSyntax.To(descriptor.ImplementationType), descriptor.Lifetime); + result = ConfigureLifecycle(bindingToSyntax.To(adapter.ImplementationType), adapter.Lifetime); } - else if (descriptor.ImplementationFactory != null) + else if (adapter.UseServiceFactory) { result = ConfigureLifecycle(bindingToSyntax.ToMethod(context @@ -70,18 +104,16 @@ private IBindingWithOrOnSyntax ConfigureImplementationAndLifecycle( // correct _scoped_ IServiceProvider is used. Fall back to root IServiceProvider when not created // through a NinjectServiceProvider (some tests do this to prove a point) var scopeProvider = context.GetServiceProviderScopeParameter()?.SourceServiceProvider ?? context.Kernel.Get(); - return descriptor.ImplementationFactory(scopeProvider) as T; - }), descriptor.Lifetime); + return adapter.InstantiateFromServiceFactory(scopeProvider, context) as T; + }), adapter.Lifetime); } else { // use ToMethod here as ToConstant has the wrong return type. - result = bindingToSyntax.ToMethod(context => descriptor.ImplementationInstance as T).InSingletonScope(); + result = bindingToSyntax.ToMethod(context => adapter.ImplementationInstance as T).InSingletonScope(); } - return result - .WithMetadata(nameof(ServiceDescriptor), descriptor) - .WithMetadata(nameof(BindingIndex), bindingIndex.Next(descriptor.ServiceType)); + return result; } private IBindingNamedWithOrOnSyntax ConfigureLifecycle( diff --git a/src/Ninject.Web.AspNetCore/ServiceKey.cs b/src/Ninject.Web.AspNetCore/ServiceKey.cs new file mode 100644 index 0000000..addbb93 --- /dev/null +++ b/src/Ninject.Web.AspNetCore/ServiceKey.cs @@ -0,0 +1,20 @@ +using System; + +namespace Ninject.Web.AspNetCore +{ + + /// + /// Used to store ServiceDescriptor.ServiceKey as metadata of the Ninject binding. + /// Only supported with .NET >= 8.0 + /// + public class ServiceKey + { + public object Key { get; } + + public ServiceKey(object key) + { + Key = key; + } + } + +} \ No newline at end of file