Priority: P2
Problem
ConstructorActivator treats every ServiceKey parameter as a request for the current key, even when the registration is unkeyed. In standard DI, ServiceKey injection applies when the effective service key is non-null; otherwise the parameter resolves ordinarily. Adding an unrelated DependsOn label override silently changes an injected string from its registered value to null, and rejects an otherwise registered integer.
Follow-up to closed #51; NativeConstructorActivator already guards ServiceKey injection with serviceKey != null, but ConstructorActivator does not.
Source.
Reproduction
Save this project as artifacts/review-unkeyed-servicekey/repro.csproj, with Program.cs beside it, in the reviewed checkout.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFrameworks>net472;net8.0;net9.0;net10.0</TargetFrameworks><LangVersion>14</LangVersion><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><WarningLevel>0</WarningLevel></PropertyGroup>
<ItemGroup><ProjectReference Include="../../src/Mammoth.Extensions.DependencyInjection/Mammoth.Extensions.DependencyInjection.csproj" /></ItemGroup>
</Project>
using Mammoth.Extensions.DependencyInjection;
using Mammoth.Extensions.DependencyInjection.Configuration;
using Microsoft.Extensions.DependencyInjection;
foreach (var mapped in new[] { false, true })
{
Run("string, mapped=" + mapped, () => {
var s = new ServiceCollection(); s.AddSingleton("ordinary");
if (mapped) s.AddTransient<StringConsumer>([Dependency.OnValue("label", "configured")]);
else s.AddTransient<StringConsumer>();
using var p = s.BuildServiceProvider(); return p.GetRequiredService<StringConsumer>().Key ?? "NULL";
});
Run("int, mapped=" + mapped, () => {
var s = new ServiceCollection(); s.AddSingleton("ordinary"); s.AddSingleton(typeof(int), 42);
if (mapped) s.AddTransient<IntConsumer>([Dependency.OnValue("label", "configured")]);
else s.AddTransient<IntConsumer>();
using var p = s.BuildServiceProvider(); return p.GetRequiredService<IntConsumer>().Key.ToString();
});
}
static void Run(string name, Func<string> action) { try { Console.WriteLine(name + " => " + action()); } catch(Exception e) { Console.WriteLine(name + " => " + e.GetType().Name + ": " + e.Message); } }
public sealed class StringConsumer([ServiceKey] string key, string label) { public string? Key { get; } = key; }
public sealed class IntConsumer([ServiceKey] int key, string label) { public int Key { get; } = key; }
Run dotnet run --project artifacts/review-unkeyed-servicekey/repro.csproj -c Release -f net472, then repeat with net8.0, net9.0 and net10.0.
Observed versus expected
The native paths print ordinary (string) and 42 (integer). The mapped string path prints NULL; the mapped integer path throws No satisfiable public constructor. Results match on net472/net8.0/net9.0/net10.0.
Honor ServiceKey only in a keyed context. In an unkeyed/null-key context, preserve normal registered-service and optional-default behavior, while keeping a named override for that parameter highest priority. Cover string/object/value-type ServiceKey parameters, explicit null service keys, named overrides, optional defaults and genuine non-null keyed resolutions.
Verification
Executed against ead376e with Microsoft.Extensions.DependencyInjection 10.0.0, targeting net472 on Windows .NET Framework 4.8.9345.0, .NET 8.0.31, .NET 9.0.20 and .NET 10.0.12. The larger native-comparison harness also reproduced the failure with published Mammoth 0.8.0 on net472 and .NET 10; this is pre-existing behavior, not a newly introduced performance regression. No library source changes were made.
Prepared with OpenAI Codex.
Priority: P2
Problem
ConstructorActivator treats every ServiceKey parameter as a request for the current key, even when the registration is unkeyed. In standard DI, ServiceKey injection applies when the effective service key is non-null; otherwise the parameter resolves ordinarily. Adding an unrelated DependsOn label override silently changes an injected string from its registered value to null, and rejects an otherwise registered integer.
Follow-up to closed #51; NativeConstructorActivator already guards ServiceKey injection with serviceKey != null, but ConstructorActivator does not.
Source.
Reproduction
Save this project as
artifacts/review-unkeyed-servicekey/repro.csproj, withProgram.csbeside it, in the reviewed checkout.Run
dotnet run --project artifacts/review-unkeyed-servicekey/repro.csproj -c Release -f net472, then repeat with net8.0, net9.0 and net10.0.Observed versus expected
The native paths print ordinary (string) and 42 (integer). The mapped string path prints NULL; the mapped integer path throws No satisfiable public constructor. Results match on net472/net8.0/net9.0/net10.0.
Honor ServiceKey only in a keyed context. In an unkeyed/null-key context, preserve normal registered-service and optional-default behavior, while keeping a named override for that parameter highest priority. Cover string/object/value-type ServiceKey parameters, explicit null service keys, named overrides, optional defaults and genuine non-null keyed resolutions.
Verification
Executed against ead376e with Microsoft.Extensions.DependencyInjection 10.0.0, targeting net472 on Windows .NET Framework 4.8.9345.0, .NET 8.0.31, .NET 9.0.20 and .NET 10.0.12. The larger native-comparison harness also reproduced the failure with published Mammoth 0.8.0 on net472 and .NET 10; this is pre-existing behavior, not a newly introduced performance regression. No library source changes were made.
Prepared with OpenAI Codex.