-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Fix consistency issue in GetKeyedServices with AnyKey #97561
Conversation
Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection Issue DetailsAs 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
|
Note: the feedback from #95582 (comment) states:
|
...raries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln
Outdated
Show resolved
Hide resolved
...ns.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs
Show resolved
Hide resolved
Yes I still need to update that part, I can do it in this PR too. |
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)); |
There was a problem hiding this comment.
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.
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,
There was a problem hiding this comment.
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<>
.
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs
Show resolved
Hide resolved
This pull request has been automatically marked |
Reopened based on #95853 (comment): |
This pull request has been automatically marked |
This pull request will now be closed since it had been marked |
So is anything happening with this? |
Re-opened via #113137 |
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.