Skip to content

Unkeyed DependsOn injects null or rejects registered ServiceKey parameter types #112

Description

@AGiorgetti

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.

Activity

  1. AGiorgetti commented on Oct 9, 2026

    @AGiorgetti
    ContributorAuthor

    Implemented in draft PR #119: #119

    The defect came from treating [ServiceKey] as contextual injection when the effective key was null. Constructor selection could reject a registered int parameter, while argument resolution silently supplied null for string/object parameters. Both paths now require a non-null effective key before injecting it. Without one, the existing ordinary-registration/default path applies, matching native Microsoft DI. For example, a registered int value 42 is now supplied to an unkeyed mapped constructor even when its key parameter has [ServiceKey].

    Named value/key overrides still run first; genuine non-null keys retain type validation. Registered null factory results do not become optional defaults, missing required registrations fail before unrelated activation, and factory exceptions pass through unchanged. The fix adds no reflection and updates the README and vNext changelog.

    Tests were committed first. The 240 new cases per target produced 138 failures and 102 passing controls before the fix on all four targets. Four older cases asserted the defect's null-injection behavior; corrected native expectations also failed before the fix, with their two keyed controls passing.

    Verification for final commit e8fc2422cf02dffb60361d12df848361bbb484df:

    • Local Windows restore and Release CI-style build passed.
    • Local and Windows CI full suites each passed 2,497 tests on net472/net8.0/net9.0/net10.0: 9,988 passed, zero failed/skipped per full run.
    • Local and CI native DI 10.0.0 cache probes passed on all four targets, each with 100 interleaved enumerations after observed compilation.
    • Whitespace check passed. No applicable checks were unavailable.
    • The build has only the two existing net472 support warnings from Microsoft.Extensions.Telemetry.Abstractions and Microsoft.Extensions.Diagnostics.Testing 10.0.0, with no compiler/analyzer warnings or errors.
    • Exact-commit CI: https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/actions/runs/37905150220 (success). Pack and Publish were skipped.

    Paused for review. This is the last bug in the original #106–#112 list; after review and merge, a release-validation checkpoint is appropriate. No release actions were taken.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions