Skip to content

Keyed decorator inner binding consumes an explicitly keyed constructor dependency #109

Description

@AGiorgetti

Priority: P2

Problem

Contextual keyed decorator activation binds the supplied inner instance to the first assignable parameter, excluding only ServiceKey. An earlier same-service-type parameter marked FromKeyedServices therefore consumes the inner instance instead of resolving its specified key. Reordering the otherwise equivalent constructor to put inner first makes resolution succeed.

Follow-up to closed #51: ServiceKey parameters are excluded from inner binding, but explicitly keyed dependencies are still eligible.

Source: ConstructorActivator.cs:49.

Reproduction

Create artifacts/review-decorator-binding/repro.csproj and Program.cs in a checkout of the pinned commit. The project uses the same DI 10.0.0 references as the library.

<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>
  </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 otherFirst in new[] { true, false })
    Report(otherFirst ? "other first" : "inner first", () => {
        var s = new ServiceCollection();
        s.AddKeyedSingleton<IWork, Work>("other");
        s.AddKeyedTransient<IWork, Work>("blue");
        if (otherFirst) s.Decorate<IWork, OtherFirst>();
        else s.Decorate<IWork, InnerFirst>();
        using var p = s.BuildServiceProvider();
        Console.WriteLine(p.GetRequiredKeyedService<IWork>("blue").GetType().Name);
    });
static void Report(string path, Action action)
{
    try { action(); Console.WriteLine(path + ": OK"); }
    catch (Exception e) { Console.WriteLine(path + ": " + e.GetType().Name + ": " + e.Message); }
}
public interface IWork { }
public sealed class Work : IWork { }
public sealed class OtherFirst([FromKeyedServices("other")] IWork other,
    IWork inner, [ServiceKey] string key) : IWork { }
public sealed class InnerFirst(IWork inner,
    [FromKeyedServices("other")] IWork other, [ServiceKey] string key) : IWork { }

Run dotnet run --project artifacts/review-decorator-binding/repro.csproj -c Release -f net472; repeat with net8.0, net9.0 and net10.0.

Observed versus expected

With OtherFirst, blue resolution throws No satisfiable public constructor on OtherFirst. With InnerFirst, blue resolution succeeds. Both blue and other services are registered. The failure is also present in published 0.8.0.

Do not bind the decoration inner instance to a parameter requiring a different explicit keyed dependency. Preserve explicit keyed lookup and actual ServiceKey injection independently of parameter order. Test reference identities, repeated layers, all lifetimes, inherited/explicit keys and disposal ownership.

Verification

Reviewed source: ead376e. Executed matching library targets on net472 (Windows .NET Framework 4.8.9345.0), .NET 8.0.31, .NET 9.0.20 and .NET 10.0.12. Also compared the original probes against published Mammoth 0.8.0 on net472 and .NET 10. These are executable comparison probes, not new committed regression tests. No library source changes were made.

Prepared with OpenAI Codex.

Activity

  1. AGiorgetti commented on Oct 8, 2026

    @AGiorgetti
    ContributorAuthor

    Implemented #109 in draft PR #116, final commit e5a2ca47bd0554a5b6da000216651b0bc144c1da.

    The decorator activator previously bound the supplied inner service to the first assignable parameter, even when that parameter had FromKeyedServices. An earlier same-interface or object-typed keyed dependency could therefore consume the inner instance, causing constructor rejection or silently swapped identities. The fix excludes those attributed parameters from inner binding, retaining the existing ServiceKey exclusion. The ordinary parameter receives the inner; explicit, inherited and null-key dependencies retain their own lookup, and optional defaults remain available.

    The production fix is one additional cached-metadata check with an explanatory code comment; the changelog is updated under vNext Bug Fixes.

    Verification:

    • Corrected regression tests were committed before production changes (508bbe6, ec100d8): 93 failed / 84 passed per target on unchanged production code.
    • Final Release restore/build passed across library targets netstandard2.0/net8.0/net9.0/net10.0.
    • Full Windows suite: 1,864 passed / 0 failed / 0 skipped on each of net472/net8.0/net9.0/net10.0 (7,456 total). The 177 new cases cover both parameter orders, explicit/inherited/null keys, interface/object parameters, native/snapshot/diagnostic providers, type/factory/caller-owned registrations, every lifetime, repeated layers, reference identities, scope reuse, disposal ownership, optional defaults and rejected constructors without dependency-factory activation. Native controls verify expected implementation/key and dependency identities.
    • Native DI 10.0.0 cache probes passed on all four targets: 100 interleaved enumerations each after observed native compilation.
    • CRLF-aware diff check passed.
    • CI run 37790547876 succeeded for the exact final commit, with 1,864 passing tests per target and all four native-cache probes passing. Pack and Publish were skipped.
    • The two existing net472 package-support warnings remain for Microsoft.Extensions.Telemetry.Abstractions and Microsoft.Extensions.Diagnostics.Testing 10.0.0. No new compiler/analyzer warnings, failed final checks or blocked checks remain.

    Awaiting your review and explicit instruction before starting another issue.

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