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

KeyedService.AnyKey is not usable with ServiceKeyAttribute on object-typed arguments due to type validation #93438

Open
DillonN opened this issue Oct 13, 2023 · 14 comments
Assignees
Labels
area-Extensions-DependencyInjection enhancement Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@DillonN
Copy link

DillonN commented Oct 13, 2023

Description

I'm trying to use the new Keyed services feature, but it looks like the KeyedService.AnyKey piece is not functional when used in conjunction with ServiceKeyAttribute?

E: Issue only occurs when trying to use a base class for the ServiceKeyAttribute argument, e.g. in the case below where object is the argument.

Whenever I try, all combination of types leads to an exception:

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

This appears to be due to a check that requires the parameter's type to be the same as the one on the registration:

if (parameterType != serviceIdentifier.ServiceKey.GetType())
{
throw new InvalidOperationException(SR.InvalidServiceKeyType);
}

However, this is impossible because KeyedService.AnyKey uses a dedicated private type and is only exposed as object.

I feel like I'm missing something here, but how does KeyedService.AnyKey work with that strict type check in place?

Reproduction Steps

Create a new console app under .NET 8 RC2, add the Microsoft.Extensions.DependencyInjection package, and paste this into Program.cs:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddKeyedTransient<Service>(KeyedService.AnyKey);

// This line throws
// System.InvalidOperationException: 'The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute.'
using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });

// If ValidateOnBuild is false, this line also throws
// System.InvalidOperationException: 'The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute.'
var service = provider.GetKeyedService<Service>("test");

public class Service
{
    public Service([ServiceKey] object serviceKey)
    { }
}

Expected behavior

  • When using KeyedService.AnyKey for a service that uses the [ServiceKey] attribute, building the service provider should succeed without validation failures
  • When using KeyedService.AnyKey for a service that uses the [ServiceKey] attribute, resolving the service using any key input should return the registered implementation and fill in the service key value for the marked parameter

Actual behavior

In both of the above points, a System.InvalidOperationException is thrown instead

Regression?

No response

Known Workarounds

I can workaround by using a factory service registration instead:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddKeyedTransient<Service>(KeyedService.AnyKey, (_, key) => new Service(key));

// Validation succeeds now
using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });

// Resolving service also succeeds now
var service = provider.GetKeyedService<Service>("test");

public class Service
{
    public Service(object serviceKey)
    { }
}

Configuration

.NET SDK:
 Version:   8.0.100-rc.2.23502.2
 Commit:    0abacfc2b6

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.23550
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\8.0.100-rc.2.23502.2\

.NET workloads installed:
<removed for brevity>

Host:
  Version:      8.0.0-rc.2.23479.6
  Architecture: x64
  Commit:       0b25e38ad3

.NET SDKs installed:
  8.0.100-rc.2.23502.2 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
<removed for brevity>
  Microsoft.NETCore.App 8.0.0-rc.2.23479.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
<removed for brevity>

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Oct 13, 2023
@ghost
Copy link

ghost commented Oct 13, 2023

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

Issue Details

Description

I'm trying to use the new Keyed services feature, but it looks like the KeyedService.AnyKey piece is not functional when used in conjunction with ServiceKeyAttribute?

Whenever I try, all combination of types leads to an exception:

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

This appears to be due to a check that requires the parameter's type to be the same as the one on the registration:

if (parameterType != serviceIdentifier.ServiceKey.GetType())

However, this is impossible because KeyedService.AnyKey uses a dedicated private type and is only exposed as object.

I feel like I'm missing something here, but how does KeyedService.AnyKey work with that strict type check in place?

Reproduction Steps

Create a new console app under .NET 8 RC2, add the Microsoft.Extensions.DependencyInjection package, and paste this into Program.cs:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddKeyedTransient<Service>(KeyedService.AnyKey);

// This line throws
// System.InvalidOperationException: 'The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute.'
using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });

// If ValidateOnBuild is false, this line also throws
// System.InvalidOperationException: 'The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute.'
var service = provider.GetKeyedService<Service>("test");

public class Service
{
    public Service([ServiceKey] object serviceKey)
    { }
}

Expected behavior

  • When using KeyedService.AnyKey for a service that uses the [ServiceKey] attribute, building the service provider should succeed without validation failures
  • When using KeyedService.AnyKey for a service that uses the [ServiceKey] attribute, resolving the service using any key input should return the registered implementation and fill in the service key value for the marked parameter

Actual behavior

In both of the above points, a System.InvalidOperationException is thrown instead

Regression?

No response

Known Workarounds

I can workaround by using a factory service registration instead:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddKeyedTransient<Service>(KeyedService.AnyKey, (_, key) => new Service(key));

// Validation succeeds now
using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });

// Resolving service also succeeds now
var service = provider.GetKeyedService<Service>("test");

public class Service
{
    public Service(object serviceKey)
    { }
}

Configuration

.NET SDK:
 Version:   8.0.100-rc.2.23502.2
 Commit:    0abacfc2b6

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.23550
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\8.0.100-rc.2.23502.2\

.NET workloads installed:
<removed for brevity>

Host:
  Version:      8.0.0-rc.2.23479.6
  Architecture: x64
  Commit:       0b25e38ad3

.NET SDKs installed:
  8.0.100-rc.2.23502.2 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
<removed for brevity>
  Microsoft.NETCore.App 8.0.0-rc.2.23479.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
<removed for brevity>

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

Other information

No response

Author: DillonN
Assignees: -
Labels:

untriaged, area-Extensions-DependencyInjection

Milestone: -

@steveharter
Copy link
Member

PTAL @benjaminpetit

@benjaminpetit
Copy link
Member

provider.GetKeyedService<Service>("test"); will not throw if you change the type of the key in your constructor to string instead of object. The type check might be a bit too restrictive here...

For services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });, it seems to be a bug yes, I will investigate.

@steveharter steveharter added bug and removed untriaged New issue has not been triaged by the area owner labels Oct 18, 2023
@steveharter steveharter added this to the 9.0.0 milestone Oct 18, 2023
@DillonN
Copy link
Author

DillonN commented Oct 19, 2023

@benjaminpetit thanks, I didn't notice that, was too focused with having object for the key type. I guess with the current check, doing that does not make sense? As the only thing I can get to work is provider.GetKeyedService<Service>(new object());. Any type that is not exactly System.Object will fail.

@benjaminpetit
Copy link
Member

Yes, the type check is strict: if you are asking for an object in your constructor, the key provided must be an object. If you are asking for a string, then only a string can be provided. But we could allow inheritance IMO (your example could work for example).

@steveharter
Copy link
Member

@benjaminpetit based on

provider.GetKeyedService("test"); will not throw if you change the type of the key in your constructor to string instead of object. The type check might be a bit too restrictive here...

For services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });, it seems to be a bug yes, I will investigate.

and then later

Yes, the type check is strict: if you are asking for an object in your constructor, the key provided must be an object. If you are asking for a string, then only a string can be provided. But we could allow inheritance IMO (your example could work for example).

I assume the type check is strict by design, but we could loosen it up by supporting inheritance?

@steveharter steveharter added the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Jul 25, 2024
@steveharter steveharter modified the milestones: 9.0.0, 10.0.0 Jul 25, 2024
@drolevar
Copy link

@steveharter Just bumped into the issue myself. I guess the type check is not necessary when AnyKey is used. Because semantically AnyKey is exactly what it is - any key.

Another (stricter) approach could be limit AnyKey usage to object-typed parameters and to to introduce a generic AnyKeyObj for other parameters. But that would be a breaking change.

@drpdrp
Copy link

drpdrp commented Aug 2, 2024

I'm experiencing inverted problem. Trying to get all registered keyed services, but getting empty list. How can I get all registered keyed services from service provider?

registration:
services.AddKeyedSingleton<IMyType, MyType1>(typeof(MyType1).ToString());
services.AddKeyedSingleton<IMyType, MyType2>(typeof(MyType2).ToString());

access:
var list = provider.GetKeyedServices<IMyType>(KeyedService.AnyKey) // getting []

this works:
provider.GetRequiredKeyedService<IMyType>(typeof(MyType1).ToString())
provider.GetRequiredKeyedService<IMyType>(typeof(MyType2).ToString())

@davidfowl
Copy link
Member

This is something we should fix in .NET 10.

@DillonN
Copy link
Author

DillonN commented Dec 28, 2024

It would be a welcome change! I still have the factory registration workaround setup in a few places

@DillonN DillonN changed the title KeyedService.AnyKey is not usable with ServiceKeyAttribute due to type validation KeyedService.AnyKey is not usable with ServiceKeyAttribute on object-typed arguments due to type validation Dec 28, 2024
@steveharter
Copy link
Member

I'm experiencing inverted problem. Trying to get all registered keyed services, but getting empty list. How can I get all registered keyed services from service provider?
...
var list = provider.GetKeyedServices(KeyedService.AnyKey) // getting []
...

This was fixed in v9: #95582 (you should get back a 2-element array based on MyType1 and MyType2) however it was not backported to v8 due to unresolved issues which are being addressed in v10 via #113137.

@steveharter steveharter removed the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Mar 5, 2025
@steveharter steveharter modified the milestone: 10.0.0 Mar 5, 2025
@steveharter
Copy link
Member

Based on the discussions above, supporting inheritance for a service constructor parameter using ServiceKeyAttribute seems like the correct approach. If that is not feasible for some reason, a fallback approach would be to just support object.

@steveharter
Copy link
Member

@davidfowl

This is something we should fix in .NET 10.

Do you mean the comment just above yours regarding GetKeyedServices<IMyType>(KeyedService.AnyKey) (which was fixed in v9) or the original issue here (support for object as parameter type if AnyKey is used)?

@steveharter steveharter added enhancement Product code improvement that does NOT require public API changes/additions and removed bug labels Mar 6, 2025
@ReubenBond
Copy link
Member

ReubenBond commented Mar 27, 2025

I hit this today. In my case, I cannot work around it because I have an open generic registration (typeof(IDurableList<>)) and therefore cannot use a factory. If an assignability check during validation (i.e, ValidateOnBuild is set) is considered too costly (IMHO it shouldn't be) then I could use [ServiceKey] object key and perform my own runtime validation that the key is a string.

I believe this is a bug. M.E.DI has tests which exercise this behavior (eg ResolveKeyedServiceTransientTypeWithAnyKey), but we do not set ValidateOnBuild in our tests, so this won't be hit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Extensions-DependencyInjection enhancement Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

No branches or pull requests

7 participants