Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix consistency issue in GetKeyedServices with AnyKey #97561

Closed

Conversation

benjaminpetit
Copy link
Member

As noted in #95582, @halter73 noticed that there was some inconsistency, because a call to enumerate all services will create some cache.

In this PR, we make sure that when enumerating all services with KeyedService.AnyKey, we create matching callsite that matches the real descriptor identifier.

@ghost
Copy link

ghost commented Jan 26, 2024

Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection
See info in area-owners.md if you want to be subscribed.

Issue Details

As noted in #95582, @halter73 noticed that there was some inconsistency, because a call to enumerate all services will create some cache.

In this PR, we make sure that when enumerating all services with KeyedService.AnyKey, we create matching callsite that matches the real descriptor identifier.

Author: benjaminpetit
Assignees: -
Labels:

area-Extensions-DependencyInjection

Milestone: -

@steveharter
Copy link
Member

steveharter commented Jan 26, 2024

Note: the feedback from #95582 (comment) states:

  • Only include all matching AnyKey-registered services if there are no null-registered services.
    • This is equivalent to what we do for keys that are not AnyKey or null.
  • Always include all matching AnyKey-registered services after null-registered services.
    • Then we should update the behavior for keys that are not AnyKey or null to match.
  • Continue to say the AnyKey only applies to non-null keys
    • This is probably more surprising but simpler to explain.

About not including the registration with KeyedService.AnyKey: it would make things simpler (for me at least) but will be very confusing for the user.

I think it's simpler and easier to explain to not return non-keyed services in GetKeyedServices(KeyedService.AnyKey). It's already very confusing, and the developer can call GetKeyedServices and GetServices to get both.

@benjaminpetit
Copy link
Member Author

Note: the feedback from #95582 (comment) states:

  • Only include all matching AnyKey-registered services if there are no null-registered services.

    • This is equivalent to what we do for keys that are not AnyKey or null.
  • Always include all matching AnyKey-registered services after null-registered services.

    • Then we should update the behavior for keys that are not AnyKey or null to match.
  • Continue to say the AnyKey only applies to non-null keys

    • This is probably more surprising but simpler to explain.

About not including the registration with KeyedService.AnyKey: it would make things simpler (for me at least) but will be very confusing for the user.

I think it's simpler and easier to explain to not return non-keyed services in GetKeyedServices(KeyedService.AnyKey). It's already very confusing, and the developer can call GetKeyedServices and GetServices to get both.

Yes I still need to update that part, I can do it in this PR too.

@steveharter steveharter requested a review from halter73 January 31, 2024 17:19
Assert.Equal(5, allServices.Count);
Assert.Equal(new[] { service1, service2, service3, service4 }, allServices.Skip(1));
Assert.Equal(4, allServices.Count);
Assert.Equal(new[] { service1, service2, service3, service4 }, allServices);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Assert.Equal(new[] { service1, service2, service3, service4 }, allServices);
Assert.Equal(new[] { service1, service2, service3, service4 }, allServices);
var someKeyedServices = provider.GetKeyedServices<IService>("service").ToList();
Assert.Equal(new[] { service2, service3, service4 }, someKeyedServices);
var unkeyedServices = provider.GetServices<IService>().ToList();
Assert.Equal(new[] { service5, service6 }, unkeyedServices);

Nit: I'm assuming this all works, and some of this might even be covered partially by other tests. But we don't have many tests with the combination of AnyKey registrations, unkeyed registrations, and enumerable resolution of both keyed and unkeyed services. And I don't see the harm in extra verification.

// Check twice in different order to check caching
var provider2 = CreateServiceProvider(serviceCollection);
Assert.Equal(new[] { service }, provider2.GetKeyedServices<IService>(KeyedService.AnyKey));
Assert.Same(any, provider2.GetKeyedService<IService>(KeyedService.AnyKey));
Copy link
Member

Choose a reason for hiding this comment

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

Now that the AnyKey has special status for enumerable lookups, I think we should be careful not to imply it's doing anything special for single service resolution. The simplest thing is to simply not allow it, and only allow it for service registration and enumerable lookups.

If you have [ServiceKey] string key in the constructor of your AnyKey service, the above line will already throw, so it's not something a library or framework developer can rely on always working anyway.

System.InvalidOperationException: The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute.

Given this, and that you can just use new object() instead if you really want to get the AnyKey registration for a service and you know it will not blow up on plain object instance, I see no reason not to always throw an InvalidOperationException when someone attempts to resolve a single service using the AnyKey. Then we also don't have to explain why the service returned when using AnyKey for single service resolution isn't included in the enumerable lookup.

Technically this is breaking like any behavior change is, but I would be surprised if anything other than our tests specifically try to resolve a single service with the AnyKey. And if anyone is doing that, they should probably stop and just use something else like new object() or "" which is easier to reason about.

Suggested change
Assert.Same(any, provider2.GetKeyedService<IService>(KeyedService.AnyKey));
Assert.ThrowsAny<InvalidOperationException>(() => provider2.GetKeyedService<IService>(KeyedService.AnyKey));
Assert.Same(any, provider2.GetKeyedService<IService>(new object()));

Less importantly, it also leaves the door open for us retconning what the AnyKey in single service resolution means. Maybe in .NET 20, we decide that it should return a non-keyed service if there are no keyed service registrations for a given type,

Copy link
Member

@steveharter steveharter Mar 6, 2025

Choose a reason for hiding this comment

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

I agree that we should throw on GetKeyedService(AnyKey) and also agree on the previously discussed hiding of the AnyKey registration when querying with IEnumerable<>.

@steveharter steveharter added the needs-author-action An issue or pull request that requires more info or actions from the author. label Feb 14, 2024
@ghost ghost added the no-recent-activity label Feb 29, 2024
@ghost
Copy link

ghost commented Feb 29, 2024

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@stephentoub
Copy link
Member

Reopened based on #95853 (comment):
"we first need to reopen and finish #97561 (I don't think we have consensus right now on the right approach)"

Copy link
Contributor

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

Copy link
Contributor

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

@robertmclaws
Copy link

So is anything happening with this?

@github-actions github-actions bot locked and limited conversation to collaborators Jun 5, 2024
@steveharter steveharter self-assigned this Mar 3, 2025
@steveharter
Copy link
Member

So is anything happening with this?

Re-opened via #113137

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Extensions-DependencyInjection needs-author-action An issue or pull request that requires more info or actions from the author. no-recent-activity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants