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.


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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As Microsoft DI can only resolve IEnumerable this should be fine.

/// <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.

@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...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All of the new tests are ok now beside of 4 which are really too stupid and we don't want to support these.

…, but resolved with a specific key by using a missing binding resolver
…eyedservice constructor argument can't be resolved. Also a change for GetRequiredService as behaviour was not the same and compliance tests were not testing this good
@DominicUllmann DominicUllmann force-pushed the feature/IssueNr11_IKeyedServiceProvider branch from 377fafd to 4abe129 Compare December 31, 2025 15:31
@DominicUllmann
Copy link
Collaborator Author

I managed to fullfill all the compliance tests but 4. I think these 4 are just wrong and stupidly enforce an implemntation detail:

image

The 4 failing tests enforce that the enumarble returned when querying the same list of instances is the same instead of just equal. Some other tests doing list queries just compare for equality instead of same list instance, which would make much more sense.

So I would say the current code fullfills all the tests.

@DominicUllmann
Copy link
Collaborator Author

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.

Another difference here is that Microsoft DI can only resolve lists based on IEnumerable. The others just return null:

		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>;
		var list1 = app.Services.GetKeyedService(typeof(IEnumerable<IWarrior>), "Warrior") as IEnumerable<IWarrior>;
		var list1a = app.Services.GetKeyedService(typeof(IList<IWarrior>), "Warrior");
		var list1b = app.Services.GetKeyedService(typeof(IWarrior[]), "Warrior");

i.e. list1a and list1b return null

@DominicUllmann
Copy link
Collaborator Author

I managed to fullfill all the compliance tests but 4. I think these 4 are just wrong and stupidly enforce an implemntation detail:

image The 4 failing tests enforce that the enumarble returned when querying the same list of instances is the same instead of just equal. Some other tests doing list queries just compare for equality instead of same list instance, which would make much more sense.

So I would say the current code fullfills all the tests.

I have put some ignores so that these 4 tests are not run by dotnet test. By this everything is green now.

}
catch (ActivationException ex)
{
throw new InvalidOperationException($"Can't resolve service of Type {serviceType}", ex);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The compliance test for non keyed services is really stupid. It checked that ActivatorUtilities.Activate returns InvalidOperationException, but it doesn't call GetRequiredService directly. But checking the implemenation it throws InvalidOperationException. So for consistency, I adapted here as well.

@DominicUllmann DominicUllmann force-pushed the feature/IssueNr11_IKeyedServiceProvider branch from 2296d48 to 3e6dfa9 Compare December 31, 2025 16:17

namespace Ninject.Web.AspNetCore.RequestActivation
{
public class KeyedRequest : IRequest
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I need this special request to have the option to get the ServiceKey in the AnyBindingResolver in the end.

#if NET8_0_OR_GREATER
private object ResolveFromKeyedService(IContext parent, FromKeyedServicesAttribute keyedattribute)
{
var fromKeyedServiceValue = DeterimeFromKeyedServiceValue(keyedattribute, parent.Parameters);
Copy link
Collaborator Author

@DominicUllmann DominicUllmann Dec 31, 2025

Choose a reason for hiding this comment

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

Have to adapt the logic form base.ResolveWithin(parent) to correctly handle fromKeyedService attributes and default values. The implementation itself is quite similar still to the base implementation.

Copy link
Owner

@lord-executor lord-executor left a comment

Choose a reason for hiding this comment

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

Looks like you made it! (🎵 Look how far we've come my baby 🎵 / Shania Twain - You're Still The One)
Quite impressive that you managed to satisfy all of those, at times rather odd, constraints of the keyed service implementation.

Also... Happy New Year 🥳

Fun fact: If you check for KeyedService.AnyKey references on source.dot.net which includes all of ASP.NET Core, there isn't a single actual use case of that. I went to check for that because I wanted to know if this is used for anything other than getting a list of all keyed services of some type... I'm having a hard time coming up with an actual legitimate use case where I would be needing a list of all keyed services but none of the unkeyed ones 🤔.

Let's see if I caught all the big topics that this implementation has to cover:

  • actual "keyed services" that can be requested by key
  • a fundamental separation of keyed services from unkeyed ones - a separation that is underminded by the fact that using null as the service key essentially means unkeyed services...
  • the magical and mystical behavior of KeyedService.AnyKey
  • injecting the service key itself into services
  • dealing with the FromKeyedServicesAttribute

Most of what I "found" in the code review is how stupid some of the compliance tests are and how an actual specification or documentation would have been nice. There are a whole lot of very awkward and implicit constraints that really should not have made it into the "design" of keyed services. I made a couple of small suggestions, but I couldn't find any actual problems with the implementation 👍 .

At this point, the changes to the standard Ninject behavior are quite extensive which makes me wonder about the future of Ninject overall. It's probably for the better that Ninject isn't being actively developed since the types of interventions that we are now making would otherwise be quite a liability for maintenance.

{
private readonly IDictionary<Type, Item> _bindingIndexMap = new Dictionary<Type, Item>();
private readonly IDictionary<ServiceTypeKey, Item> _bindingIndexMap = new Dictionary<ServiceTypeKey, Item>();
public const string DefaultIndexKey = "NonKeyed";
Copy link
Owner

Choose a reason for hiding this comment

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

I'm not sure I understand the purpose of this 🤔. The IndexedBindingPrecedenceComparer is not considering the IndexKey as far as I can tell. Is the primary reason for this key so that we don't have null keys? If so, then I would suggest instead of using a string that anybody could "accidentally" use as an actual service key, we do another "Microsoft copy-pasta bit" and define a dedicated private sealed nested type:

private sealed class DefaultIndexKey
{
    public override string? ToString() => nameof(DefaultIndexKey);
}

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 also support overriding bindings with servicekeys.
If we have a registration for IWarrior without service key and with some service keys and we override them again and again only the latest binding per service key must be considered when resolving a unique instance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But the idea with the class is good, adapted in 299b11f

@DominicUllmann
Copy link
Collaborator Author

Looks like you made it! (🎵 Look how far we've come my baby 🎵 / Shania Twain - You're Still The One) Quite impressive that you managed to satisfy all of those, at times rather odd, constraints of the keyed service implementation.

Also... Happy New Year 🥳

Fun fact: If you check for KeyedService.AnyKey references on source.dot.net which includes all of ASP.NET Core, there isn't a single actual use case of that. I went to check for that because I wanted to know if this is used for anything other than getting a list of all keyed services of some type... I'm having a hard time coming up with an actual legitimate use case where I would be needing a list of all keyed services but none of the unkeyed ones 🤔.

Let's see if I caught all the big topics that this implementation has to cover:

  • actual "keyed services" that can be requested by key
  • a fundamental separation of keyed services from unkeyed ones - a separation that is underminded by the fact that using null as the service key essentially means unkeyed services...
  • the magical and mystical behavior of KeyedService.AnyKey
  • injecting the service key itself into services
  • dealing with the FromKeyedServicesAttribute

Most of what I "found" in the code review is how stupid some of the compliance tests are and how an actual specification or documentation would have been nice. There are a whole lot of very awkward and implicit constraints that really should not have made it into the "design" of keyed services. I made a couple of small suggestions, but I couldn't find any actual problems with the implementation 👍 .

At this point, the changes to the standard Ninject behavior are quite extensive which makes me wonder about the future of Ninject overall. It's probably for the better that Ninject isn't being actively developed since the types of interventions that we are now making would otherwise be quite a liability for maintenance.

Thanks a lot, it was quite hard work to fullfill all this. But luckily Ninject is quite flexible and has extension points which allow to also fullfill such strange requirements. But yes, especially the FromKeyedServices implementation needed some advanced customizations.

@lord-executor
Copy link
Owner

Thanks a lot, it was quite hard work to fullfill all this. But luckily Ninject is quite flexible and has extension points which allow to also fullfill such strange requirements. But yes, especially the FromKeyedServices implementation needed some advanced customizations.

Nice cleanup too 👍. It all checks out as far as I can tell and I've test it on some of my projects too.

@DominicUllmann DominicUllmann merged commit 89280e1 into main Jan 3, 2026
1 check passed
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