Skip to content

Dependency injection: FromKeyedServiceAttribute falls back to last defined unkeyed service of the type -- unexpected behavior? #102654

@askogvold

Description

@askogvold

Description

I'm currently in a repo where we might need a mix of keyed and unkeyed services. For the specific case, various types of a service are configured, and injected through an IEnumerable. The keyed dependency would be used to retrieve a default one for some specific cases. We've been able to work around it, but I was surprised by the behavior, so I just want to check if this behavior is intended.

When configuring a service which has a constructor argument with the attribute [FromKeyedServices("myKey")] on an argument - if that service was not configured with the key, I will get injected the last one which was configured without a key.

This seems like a corner case, and in my mental model, I'd expect to get an exception if the argument is non-nullable, or null if it were. If the typing system makes that challenging, an option could be to add another attribute: FromRequiredKeyedService which would throw if the service is missing.

Reproduction Steps

The test called ShouldNotGetServiceIfKeyedIsNotAdded demonstrates the issue

namespace keys_repro;
using Microsoft.Extensions.DependencyInjection;

public class DemonstratingTheProblemTests
{
    [Fact]
    public void WorksIfKeyIsSpecified()
    {
        IServiceCollection services = new ServiceCollection();

        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceA"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceB"));
        services.AddKeyedTransient<IService>("myKey", (_,_) => new ServiceImplementation("KeyedService"));
        services.AddKeyedTransient<IService>("someOtherKey", (_,_) => new ServiceImplementation("AnotherKeyedService"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceC"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceD"));
        services.AddTransient<ServiceWhichAcceptsKeyedService>();

        var serviceProvider = services.BuildServiceProvider();
        var myResolvedService = serviceProvider.GetRequiredService<ServiceWhichAcceptsKeyedService>();

        Assert.Equal("KeyedService", myResolvedService.ServiceIfDefinedByKey!.Name);
    }

    [Fact]
    public void ShouldNotGetServiceIfKeyedIsNotAdded()
    {
        IServiceCollection services = new ServiceCollection();

        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceA"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceB"));
        // and no keyed service added - but continue with the rest as before
        services.AddKeyedTransient<IService>("someOtherKey", (_,_) => new ServiceImplementation("AnotherKeyedService"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceC"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceD"));

        services.AddTransient<ServiceWhichAcceptsKeyedService>();

        var serviceProvider = services.BuildServiceProvider();
        var myResolvedService = serviceProvider.GetRequiredService<ServiceWhichAcceptsKeyedService>();

        Assert.Null(myResolvedService.ServiceIfDefinedByKey); // This fails - ServiceD is resolved
    }
}

public record ServiceWhichAcceptsKeyedService([FromKeyedServices("myKey")] IService? ServiceIfDefinedByKey);

public interface IService
{
    string Name { get; }
}

public record ServiceImplementation(string Name) : IService;

Expected behavior

Test called ShouldNotGetServiceIfKeyedIsNotAdded should succeed - I would expect it not to resolve the service when I request a keyed service, and no keyed service is defined.

Actual behavior

The test called ShouldNotGetServiceIfKeyedIsNotAdded fails - I get the last configured unkeyed service of the type.

Regression?

No response

Known Workarounds

Explicitly taking a serviceProvider, and calling GetKeyedService or GetRequiredKeyedService works.

(for the types not defined in this example, see their implementation in the reproduction steps)

namespace keys_repro;

using Microsoft.Extensions.DependencyInjection;

public class WorkaroundTests
{
    [Fact]
    public void ShouldGetServiceWhenKeyedIsConfigured()
    {
        IServiceCollection services = new ServiceCollection();

        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceA"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceB"));
        services.AddKeyedTransient<IService>("myKey", (_,_) => new ServiceImplementation("KeyedService"));
        services.AddKeyedTransient<IService>("someOtherKey", (_,_) => new ServiceImplementation("AnotherKeyedService"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceC"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceD"));

        services.AddTransient<ServiceWhichAcceptsKeyServiceExplicitWorkaround>();

        var serviceProvider = services.BuildServiceProvider();
        var myResolvedService = serviceProvider.GetRequiredService<ServiceWhichAcceptsKeyServiceExplicitWorkaround>();

        Assert.Equal("KeyedService", myResolvedService.ServiceIfDefinedByKey!.Name); // This workaround works
    }

    [Fact]
    public void ShouldNotGetServiceIfKeyedIsNotAddedWithExplicitWorkaround()
    {
        IServiceCollection services = new ServiceCollection();

        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceA"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceB"));
        // and no keyed service added - but continue with the rest as before
        services.AddKeyedTransient<IService>("someOtherKey", (_,_) => new ServiceImplementation("AnotherKeyedService"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceC"));
        services.AddTransient<IService>(_ => new ServiceImplementation("ServiceD"));

        services.AddTransient<ServiceWhichAcceptsKeyServiceExplicitWorkaround>();

        var serviceProvider = services.BuildServiceProvider();
        var myResolvedService = serviceProvider.GetRequiredService<ServiceWhichAcceptsKeyServiceExplicitWorkaround>();

        Assert.Null(myResolvedService.ServiceIfDefinedByKey); // This workaround works
    }
}

public class ServiceWhichAcceptsKeyServiceExplicitWorkaround
{
    public IService? ServiceIfDefinedByKey { get; }

    public ServiceWhichAcceptsKeyServiceExplicitWorkaround(IServiceProvider serviceProvider)
    {
        ServiceIfDefinedByKey = serviceProvider.GetKeyedService<IService>("myKey");
    }
}

Configuration

<TargetFramework>net8.0</TargetFramework>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />

Other information

No response

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions