Skip to content

DependsOn changes native precedence when FromKeyedServices and ServiceKey share a parameter #122

Description

@AGiorgetti

Problem

Priority: P2 · Type: bug · Pre-existing in published 0.8.0.

For a keyed constructor parameter declaring [FromKeyedServices("fixed"), ServiceKey], native DI injects the explicitly keyed dependency. Adding a nonempty DependsOn map that does not override this parameter instead injects the owning service key.

The native control returns "dependency"; mapped activation returns "blue". The map entry is deliberately unused, exercising the documented behavior that unused map names are ignored.

ConstructorActivator tests the ServiceKey flag before FromKeyedServices lookup and returns the service key directly. Native-compatible original decorator activation already processes attributes in metadata order; the nonempty mapped path applies a different precedence.

Reproduction

Save the following files under artifacts/release-review-20261009/issue-probes/key-attribute-order/ in the reviewed checkout. Build with a .NET 10 SDK.

repro.csproj:

<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 Condition="'$(PublishedBaseline)' != 'true'"><ProjectReference Include="../../../../src/Mammoth.Extensions.DependencyInjection/Mammoth.Extensions.DependencyInjection.csproj" /></ItemGroup>
  <ItemGroup Condition="'$(PublishedBaseline)' == 'true'"><PackageReference Include="Mammoth.Extensions.DependencyInjection" Version="0.8.0" /></ItemGroup>
</Project>

Program.cs:

using Mammoth.Extensions.DependencyInjection;
using Mammoth.Extensions.DependencyInjection.Configuration;
using Microsoft.Extensions.DependencyInjection;

foreach (var mapped in new[] { false, true })
{
    var services = new ServiceCollection();
    services.AddKeyedSingleton("fixed", "dependency");
    if (mapped)
        services.AddKeyedTransient<Consumer>("blue", [Dependency.OnValue("unused", 1)]);
    else services.AddKeyedTransient<Consumer>("blue");
    using var provider = services.BuildServiceProvider();
    Console.WriteLine("mapped=" + mapped + " => " + provider.GetRequiredKeyedService<Consumer>("blue").Key);
}
public class Consumer([FromKeyedServices("fixed"), ServiceKey] string key)
{
    public string Key => key;
}

Run dotnet run --project artifacts/release-review-20261009/issue-probes/key-attribute-order/repro.csproj -c Release -f net472; repeat for net8.0, net9.0 and net10.0. Add -p:PublishedBaseline=true to run the published 0.8.0 control.

Observed versus expected

mapped=False => dependency
mapped=True => blue

Expected: an untouched constructor parameter preserves the native binding ("dependency"). A named DependsOn override targeting that parameter must retain its documented highest priority.

Fix direction and regression coverage

Preserve native attribute precedence when both key attributes occur on a parameter, after checking named overrides. Cover both attribute orders, absent and registered explicit-key dependencies, mapped overrides of the attributed parameter versus unrelated/unused map entries, and non-null versus null/unkeyed contexts. Run the native controls and mapped registrations across supported targets and lifetimes.

Verification and related work

Executed against develop commit b531864 with Microsoft.Extensions.DependencyInjection 10.0.0, on Windows net472 (.NET Framework 4.8.9345.0), .NET 8.0.31, .NET 9.0.20 and .NET 10.0.12. Published Mammoth 0.8.0 was compared on the same targets and DI dependency. These are standalone executable comparison probes; production source was not modified.

The broader review also verified the keyed discrepancy through Mammoth snapshot and diagnostic providers. Related: closed #51 for contextual keyed activation and #112 for null/unkeyed ServiceKey handling.

Prepared with OpenAI Codex.

Activity

  1. AGiorgetti commented on Oct 9, 2026

    @AGiorgetti
    ContributorAuthor

    Implemented in draft PR #125, against current develop 571df307caf6311b1ec02f63c5134905c3f6b85e. Final feature commit: 94af8ba1eaadedb39a4075ea8c9eb5dbe547541f.

    The mapped activator retained both key attributes as independent flags and always considered ServiceKey first. Thus [FromKeyedServices("fixed"), ServiceKey] injected "blue" instead of the registered "dependency" when a nonempty map left that parameter untouched. Native DI processes the attributes in metadata order.

    The fix caches one additional metadata flag describing the attribute order. A shared predicate applies that order consistently during constructor selection and argument resolution. A preceding FromKeyedServices binding wins when its effective key is non-null, even when its dependency is missing; normal availability/default rules then apply. An explicit null lookup does not prevent a later ServiceKey from injecting a non-null owning key. Inherited keys are evaluated against the actual request, and null/unkeyed contexts still ignore ServiceKey. Named value/key overrides remain first and bypass unselected attribute binding and owning-key type checks. Contextual decorators use the same corrected metadata.

    This is a small change within the existing cached activator. It adds no service lookups, validation provider, graph planner or private DI API access. Attribute inspection uses public reflection when the existing weak type-metadata cache is populated, and effective keys remain resolution-specific. The fix does not change the native original-type activator or bring in the deferred #121 implementation. README and the vNext changelog are updated.

    Verification:

    • Tests committed before the implementation: each of net472/net8.0/net9.0/net10.0 produced 24 failures and 138 passing controls. All 162 new cases now pass on every target.
    • Native differential controls cover both attribute orders; non-null, mismatched-type, null and unkeyed contexts; registered/missing/null/throwing dependencies; required/optional parameters; unused/unrelated maps; named value/key overrides; inherited/null lookup modes; all lifetimes; repeated scopes/resolutions; activation counts; native/snapshot/diagnostic providers; and contextual decorators.
    • The exact standalone issue probe reproduces the original dependency versus blue discrepancy on current develop and published Mammoth 0.8.0 on all four targets with DI 10.0.0.
    • Local restore and Release solution build with ContinuousIntegrationBuild=True passed. An initial reused-worker build failed with access errors; disabling build servers produced a successful final build. Initial fixture/nullable compiler warnings were corrected. Final build has zero errors or compiler/analyzer warnings, with only the two existing net472 support warnings for Microsoft.Extensions.Telemetry.Abstractions and Microsoft.Extensions.Diagnostics.Testing 10.0.0.
    • Local full suite passed 2,740 tests per target: 10,960 total, zero failed/skipped. net472 ran on .NET Framework 4.8.9345.0; modern runtimes were 8.0.31, 9.0.20 and 10.0.12.
    • Local native DI 10 hot-cache probes passed on all four targets, each with 100 interleaved enumerations after observed native compilation. Whitespace check passed. No applicable final local check was unavailable.

    CI for the exact final commit 94af8ba1eaadedb39a4075ea8c9eb5dbe547541f succeeded: run 37932581363. Windows restore and Release build passed; the full suite passed 2,740 tests per target (net472/net8.0/net9.0/net10.0), 10,960 total, with zero failed/skipped. All four native DI hot-cache probes passed with observed accessor replacement and 100 interleaved enumerations. Only the same two net472 package-support warnings appeared. Pack and Publish were skipped. No applicable final verification was unavailable.

    Work stops for review of PR #125 before another issue. #121/PR #124 remain on hold. After this fix is reviewed and merged, a release-validation checkpoint should explicitly acknowledge deferred #121; passing checks here do not establish full native graph-planning parity. No merge, tag, release-branch push, package publication or release action was performed.

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