-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
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