Skip to content

Conversation

@DominicUllmann
Copy link
Collaborator

Tried to implement the keyed service provider support for >= .NET 8.0 without breaking < .NET 8.0

Sadly, there is one complication in this with resultion of IEnumerable. Ninject sadly doesn't consider metadata constraint when resolving an IEnumerable. This seems to be a potential bug in Ninject.

The following code snippet shows what Microsoft DI expects:

		var builder = WebApplication.CreateBuilder();
		builder.Services.AddKeyedTransient<IWarrior, Samurai>("Warrior");
		builder.Services.AddKeyedSingleton<IWarrior>("Warrior", new Ninja("test"));
		
		var app = builder.Build();
		var list = app.Services.GetKeyedServices(typeof(IWarrior),"Warrior") as IList<IWarrior>;
		list.Should().HaveCount(2);
		var list2 = app.Services.GetServices(typeof(IWarrior)) as IList<IWarrior>;
		var list3 = app.Services.GetRequiredService(typeof(IEnumerable<IWarrior>)) as IEnumerable<IWarrior>;
		list2.Should().HaveCount(0);
		list3.Should().HaveCount(0);

We can't resolve all the elements without considering the service key. The Microsoft DI always applies the constraint.

The PR should be extended with conversion to Array and List as well, that's not yet done. The current state is for disussion.

{
var result = _resolutionRoot.Get(serviceType);
return result;
if (!IsListType(serviceType, out var elementType))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not nice that we have to distinguish here between request for IEnumerable and just T. Sadly, this is required due to a potential bug in Ninject.
I wonder if KernelBase.UpdateRequest should not pass the constraint instead of just ignoring it in case of requesting all of a certain type.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact: Explicitly binding an IList<T> and then requesting exactly that IList<T> does not actually resolve that binding.

var kernel = new StandardKernel();
			
kernel.Bind<IWarrior>().To<Ninja>().WithConstructorArgument("name", "Nick");
kernel.Bind<IList<IWarrior>>().ToConstant(new List<IWarrior> { new Samurai() });

var warriors = kernel.Get<IList<IWarrior>>();
warriors.Count().Should().Be(1);
warriors.First().Should().BeOfType<Samurai>(); // FAIL: it's actually a Ninja

The code in Ninject.KernelBase.Resolve always treats collection type services as a special type of request. Constraints are not applied to collections, but they are also not applied to the collection items since as you mentioned the UpdateRequest just ignores them and passes null instead. If it were applied to collections, then I would call that a design decision, but since they are not applied to either of them, I'd call it a bug.


private static object ConvertToTypedEnumerable(Type elementType, IEnumerable<object> objectList)
{
var castMethod = EnumerableCastMethod.MakeGenericMethod(elementType);
Copy link
Collaborator Author

@DominicUllmann DominicUllmann Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to how Ninject does it, but I can't reuse the Ninject version directly as it is internal. Here we should also handle the case if an array or list was requested similar to how Ninject does it.

/// <summary>
/// This ServiceDescriptorAdapter is used when ServiceDescriptor.IsKeyedService == true
/// </summary>
public class KeyedDescriptorAdapter : IDescriptorAdapter
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this one to prevent code duplications in ServiceCollectionAdapter. MS decided to use Keyed* properties on the service descriptor for keyed services.
Note that the implementation factory receives the ServiceKey => that's maybe one reason for the different set of properties for keyed services.

…oject

- This is currently failing 45 out of the 54 keyed service specification
  tests
@lord-executor
Copy link
Owner

Not sure if you saw that, but there is actually a separate set of "specification tests" in the "Microsoft.Extensions.DependencyInjection.Specification" namespace just for testing keyed services. And of course, a lot of them revolve around generics which is why currently 45 out of 54 of these services fail. I think if we're trying to support keyed services, the result should somehow get all of these tests to green. This is why I abandoned that side-project when keyed services first arrived on the scene. I could not figure out a way to satisfy all of those requirements without essentially building two different Kernels, one for keyed services and one for unkeyed services.

return true;
}

if (genericTypeDefinition == typeof(IEnumerable<>))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... why is IEnumerable<> on its own while the other list-ish types are grouped together above?

@DominicUllmann
Copy link
Collaborator Author

DominicUllmann commented Dec 26, 2025

Not sure if you saw that, but there is actually a separate set of "specification tests" in the "Microsoft.Extensions.DependencyInjection.Specification" namespace just for testing keyed services. And of course, a lot of them revolve around generics which is why currently 45 out of 54 of these services fail. I think if we're trying to support keyed services, the result should somehow get all of these tests to green. This is why I abandoned that side-project when keyed services first arrived on the scene. I could not figure out a way to satisfy all of those requirements without essentially building two different Kernels, one for keyed services and one for unkeyed services.

Thanks a lot, I was wondering about that. Thanks for having added these. I have discovered a few features I have not implemented yet and also some implementation problems.
But there are really strange requirements, like e.g. ResolveKeyedServiceSingletonInstanceWithAnyKey or ResolveWithAnyKeyQuery_Constructor.
I will have to check if I can support these.

@lord-executor
Copy link
Owner

Thanks a lot, I was wondering about that. Thanks for having added these. I have discovered a few features I have not implemented yet and also some implementation problems. But there are really strange requirements, like e.g. ResolveKeyedServiceSingletonInstanceWithAnyKey or ResolveWithAnyKeyQuery_Constructor. I will have to check if I can support these.

I definitely didn't remember all of them, but I do remember that this was the best example I have ever seen that an interface on its own is just a horribly insufficient form of specification 😬. It appears that they simply made some design decisions for what would work best with their particular implementation and that then became the implicit specification.

@lord-executor
Copy link
Owner

This is also definitely interesting...

public class Service : IService
{
    private readonly string _id;

    public Service() => _id = Guid.NewGuid().ToString();

    public Service([ServiceKey] string id) => _id = id; // oh yeah, btw: the ServiceKey can be injected into the service

    public override string? ToString() => _id;
}

…binding happens; ensure that ConstructorScorer doesn't discard a constructor with ServiceKey attribute in favour of another constructor
/// This ensures that a binding with a different servicekey
/// can't override a binding with a non-matching servicekey
/// </summary>
public class ServiceTypeKey : IEquatable<ServiceTypeKey>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to split the binidngindex by servicekey as otherwise the overriding doesn't work correctly.

{
var hasInjectAttribute = constructor.HasAttribute(Settings.InjectAttribute);
var hasObsoleteAttribute = constructor.HasAttribute(typeof(ObsoleteAttribute));
var directive = new ConstructorInjectionDirectiveWithKeyedSupport(constructor, InjectorFactory.Create(constructor))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to create a ConstructorInjectionDirectiveWithKeyedSupport instead of a ConstructorDirective. Therefore we have to use an own implementation here.

protected override ITarget[] CreateTargetsFromParameters(ConstructorInfo method)
{
return method.GetParameters().
Select((Func<ParameterInfo, ParameterTarget>) (parameter => new ParameterTargetWithKeyedSupport(method, parameter))).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important here is to create a ParameterTargetWithKeyedSupport to support FromKeyedService as well as ServiceKey attribute.

return binding => {
var latest = true;
if (request.IsUnique && request.Constraint == null)
if (request.IsUnique)
Copy link
Collaborator Author

@DominicUllmann DominicUllmann Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we have now always constraints (either to exclude keyed bindings or only consider keyed bindings, we can't check for request.Constraint == null. But this should not be a big issue as Microsoft.Extension.DI bindings can't provide custom constraints. For normal ninject bindings we don't have BindingIndex, so we can always apply them here in my opinion. Do you see any reason why we have request.Constraint == null here in the first place @lord-executor ? All the existing tests still work when removing it.
Current state is that 20 of the new tests fail...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants