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

Unable to inject IKeyedServiceProvider into service with .NET 8 Preview 7 #90528

Closed
WestDiscGolf opened this issue Aug 14, 2023 · 11 comments
Closed

Comments

@WestDiscGolf
Copy link

Description

The update to dependency injection to allow for Keyed Services really interested me so I had a play.

I am unable to resolve a service which has a dependency on IKeyedServiceProvider to then try and resolve a named dependency at runtime.

This works (as it has done for years!)

    [Fact]
    public void SimpleServiceProviderResolution()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddTransient<ServiceProviderService>();
        var sp = services.BuildServiceProvider();

        // Act
        var result = sp.GetService<ServiceProviderService>();

        // Assert
        result.ServiceProvider.Should().NotBeNull();
    }

With the following Service definition

public class ServiceProviderService
{
    private readonly IServiceProvider _serviceProvider;

    public ServiceProviderService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IServiceProvider ServiceProvider => _serviceProvider;
}

In the reproduction steps I am unable to take a Service which has a dependency on IKeyedServiceProvider as it does not appear to be automatically registered when the service provider is built in the same way the IServiceProvider is registered.

Reproduction Steps

namespace KeyedServicePlayground;

public class DynamicResolvedDependencyTests
{
    [Fact]
    public void SimpleServiceKeyedResolution()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddKeyedTransient<ISimpleService, SimpleService>("simple");
        services.AddKeyedTransient<ISimpleService, AnotherSimpleService>("another");
        services.AddTransient<SimpleParentWithDynamicKeyedService>();
        var sp = services.BuildServiceProvider();
        var sut = sp.GetService<SimpleParentWithDynamicKeyedService>();

        // Act
        var result = sut!.GetService("simple");

        // Assert
        result.Should().BeOfType<SimpleService>();
    }
}

public class SimpleParentWithDynamicKeyedService
{
    private readonly IKeyedServiceProvider _serviceProvider;

    public SimpleParentWithDynamicKeyedService(IKeyedServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public ISimpleService GetService(string name) => _serviceProvider.GetKeyedService<ISimpleService>(name)!;
}

public interface ISimpleService { }

public class SimpleService : ISimpleService { }

public class AnotherSimpleService : ISimpleService { }

Expected behavior

The keyed service which has been registered with the name "simple" is resolved and returned from the GetService method on the SimpleParentWithDynamicKeyedService instance.

Actual behavior

Exception thrown:


System.InvalidOperationException
Unable to resolve service for type 'Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider' while attempting to activate 'KeyedServicePlayground.SimpleParentWithDynamicKeyedService'.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain, Int32 slot)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at KeyedServicePlayground.DynamicResolvedDependencyTests.SimpleServiceKeyedResolution() in C:\Users\<username_redacted>\source\repos\KeyedServicePlayground\KeyedServicePlayground\DirectServiceRequestTests.cs:line 155
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

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

ghost commented Aug 14, 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

The update to dependency injection to allow for Keyed Services really interested me so I had a play.

I am unable to resolve a service which has a dependency on IKeyedServiceProvider to then try and resolve a named dependency at runtime.

This works (as it has done for years!)

    [Fact]
    public void SimpleServiceProviderResolution()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddTransient<ServiceProviderService>();
        var sp = services.BuildServiceProvider();

        // Act
        var result = sp.GetService<ServiceProviderService>();

        // Assert
        result.ServiceProvider.Should().NotBeNull();
    }

With the following Service definition

public class ServiceProviderService
{
    private readonly IServiceProvider _serviceProvider;

    public ServiceProviderService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IServiceProvider ServiceProvider => _serviceProvider;
}

In the reproduction steps I am unable to take a Service which has a dependency on IKeyedServiceProvider as it does not appear to be automatically registered when the service provider is built in the same way the IServiceProvider is registered.

Reproduction Steps

namespace KeyedServicePlayground;

public class DynamicResolvedDependencyTests
{
    [Fact]
    public void SimpleServiceKeyedResolution()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddKeyedTransient<ISimpleService, SimpleService>("simple");
        services.AddKeyedTransient<ISimpleService, AnotherSimpleService>("another");
        services.AddTransient<SimpleParentWithDynamicKeyedService>();
        var sp = services.BuildServiceProvider();
        var sut = sp.GetService<SimpleParentWithDynamicKeyedService>();

        // Act
        var result = sut!.GetService("simple");

        // Assert
        result.Should().BeOfType<SimpleService>();
    }
}

public class SimpleParentWithDynamicKeyedService
{
    private readonly IKeyedServiceProvider _serviceProvider;

    public SimpleParentWithDynamicKeyedService(IKeyedServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public ISimpleService GetService(string name) => _serviceProvider.GetKeyedService<ISimpleService>(name)!;
}

public interface ISimpleService { }

public class SimpleService : ISimpleService { }

public class AnotherSimpleService : ISimpleService { }

Expected behavior

The keyed service which has been registered with the name "simple" is resolved and returned from the GetService method on the SimpleParentWithDynamicKeyedService instance.

Actual behavior

Exception thrown:


System.InvalidOperationException
Unable to resolve service for type 'Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider' while attempting to activate 'KeyedServicePlayground.SimpleParentWithDynamicKeyedService'.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain, Int32 slot)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at KeyedServicePlayground.DynamicResolvedDependencyTests.SimpleServiceKeyedResolution() in C:\Users\<username_redacted>\source\repos\KeyedServicePlayground\KeyedServicePlayground\DirectServiceRequestTests.cs:line 155
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

Author: WestDiscGolf
Assignees: -
Labels:

area-Extensions-DependencyInjection

Milestone: -

@ericstj ericstj added regression-from-last-release and removed untriaged New issue has not been triaged by the area owner labels Aug 14, 2023
@ericstj ericstj added this to the 8.0.0 milestone Aug 14, 2023
@steveharter
Copy link
Member

Note the repro above does not require transient services; singleton repros as well.

@benjaminpetit I believe CallSiteFactory.TryCreateExact(...) should result in the IKeyedService being returned but doesn't.

@benjaminpetit
Copy link
Member

We didn't expected the user to use IKeyedServiceProvider directly,all extensions methods target IServiceProvider for example.

But I guess we could add it to the list of "built in services", like we do today for IServiceProvider.

@WestDiscGolf
Copy link
Author

WestDiscGolf commented Aug 15, 2023

Oh I see what you mean with the extension methods targeting IServiceProvider.

If you change the dependency to be IServiceProvider it can be injected as expected, however when you call the GetKeyedService extension method it then throws the error "This service provider doesn't support keyed services.".

Please see example below.

public class DynamicResolvedDependencyTests
{
    [Fact]
    public void SimpleServiceKeyedResolution()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddKeyedTransient<ISimpleService, SimpleService>("simple");
        services.AddKeyedTransient<ISimpleService, AnotherSimpleService>("another");
        services.AddTransient<SimpleParentWithDynamicKeyedService>();
        var sp = services.BuildServiceProvider();
        var sut = sp.GetService<SimpleParentWithDynamicKeyedService>();

        // Act
        var result = sut!.GetService("simple");

        // Assert
        result.Should().BeOfType<SimpleService>();
    }
}

public class SimpleParentWithDynamicKeyedService
{
    private readonly IServiceProvider _serviceProvider;

    public SimpleParentWithDynamicKeyedService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public ISimpleService GetService(string name) => _serviceProvider.GetKeyedService<ISimpleService>(name)!;
}

As the above compiles, however when it runs it throws the error:

System.InvalidOperationException
This service provider doesn't support keyed services.
   at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetKeyedService[T](IServiceProvider provider, Object serviceKey)
   at KeyedServicePlayground.SimpleParentWithDynamicKeyedService.GetService(String name) in C:\Users\<username_redacted>\source\repos\KeyedServicePlayground\KeyedServicePlayground\DirectServiceRequestTests.cs:line 189
   at KeyedServicePlayground.DynamicResolvedDependencyTests.SimpleServiceKeyedResolution() in C:\Users\<username_redacted>\source\repos\KeyedServicePlayground\KeyedServicePlayground\DirectServiceRequestTests.cs:line 173
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

@benjaminpetit
Copy link
Member

It was fixed here: #89509, I don't repro this issue with a more recent build.

@WestDiscGolf
Copy link
Author

@benjaminpetit does that also relate to the extension method being on the IServiceProvider?

@benjaminpetit
Copy link
Member

Sorry if I wasn't clear:

  • IKeyedServiceProvider isn't injected, you are supposed to use IServiceProvider instead.
  • The bug you mentioned (cannot use GetKeyedService when using IServiceProvider that was resolved from DI) was fixed after preview 7

@WestDiscGolf
Copy link
Author

No worries :-)

So injecting IServiceProvider is still the expected injectable interface but the extension methods to resolve keyed services on it should now work.

Do we know if there will be a patch to preview 7 or will this be rolled into preview 8?

Thanks for taking the time to look at this :-)

@benjaminpetit
Copy link
Member

No backport for preview 7 was done, it will be included in the next preview release.

@steveharter
Copy link
Member

Closing as by design (must inject IServiceProvider, not IKeyedServiceProvider) and previously fixed (cannot use GetKeyedService when using IServiceProvider that was resolved from DI).

@WestDiscGolf
Copy link
Author

In case anyone else stumbles across this issue in preview 7 Andrew Lock demonstrates it well in this blog post - https://andrewlock.net/exploring-the-dotnet-8-preview-keyed-services-dependency-injection-support/#limitations-with-the-current-implementation

@ghost ghost locked as resolved and limited conversation to collaborators Sep 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants