Skip to content

Unkeyed decoration regresses providers without a keyed availability probe #120

Description

@AGiorgetti

Problem

Priority: P2 · Type: bug · New regression versus published 0.8.0.

Adding an unkeyed forwarding decorator now requires IServiceProviderIsKeyedService, even when the original implementation has a parameterless constructor. A provider/wrapper that supports ordinary availability probing and keyed resolution, but does not expose the keyed availability probe, succeeds before decoration and with published Mammoth 0.8.0; the current decorated registration throws.

The reproduction models that provider by passing a wrapper to registration factories. It preserves IServiceProviderIsService and IKeyedServiceProvider, including resolution of Mammoth's private decoration slots; only IServiceProviderIsKeyedService is unavailable. The ordinary native BuildServiceProvider control itself continues to expose all its normal probes.

The #111 fix routes original decorated implementation types through NativeConstructorActivator, which unconditionally requires both probes. The new keyed-probe requirement applies before examining whether any constructor actually uses a keyed dependency.

Reproduction

Save the following files under artifacts/release-review-20261009/issue-probes/keyed-probe/ 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 Microsoft.Extensions.DependencyInjection;

foreach (var decorated in new[] { false, true })
{
    IServiceCollection services = new ServiceCollection();
    services.AddTransient<IWork, Work>();
    if (decorated) services.Decorate<IWork, Wrapper>();
    // Model a provider that passes its own ordinary/keyed-resolution wrapper to factories.
    for (var i = 0; i < services.Count; i++)
    {
        var descriptor = services[i];
        if (descriptor.IsKeyedService && descriptor.KeyedImplementationFactory is { } keyedFactory)
            services[i] = ServiceDescriptor.DescribeKeyed(descriptor.ServiceType, descriptor.ServiceKey,
                (p, key) => keyedFactory(new OrdinaryProbeProvider(p), key), descriptor.Lifetime);
        else if (!descriptor.IsKeyedService && descriptor.ImplementationFactory is { } factory)
            services[i] = ServiceDescriptor.Describe(descriptor.ServiceType,
                p => factory(new OrdinaryProbeProvider(p)), descriptor.Lifetime);
    }
    try
    {
        using var provider = services.BuildServiceProvider();
        Console.WriteLine("decorated=" + decorated + " => " + provider.GetRequiredService<IWork>().GetType().Name);
    }
    catch (Exception error)
    {
        Console.WriteLine("decorated=" + decorated + " => " + error.GetType().Name + ": " + error.Message);
    }
}
public interface IWork;
public class Work : IWork;
public class Wrapper(IWork inner) : IWork;
public class OrdinaryProbeProvider(IServiceProvider provider) : IServiceProvider, IKeyedServiceProvider
{
    public object? GetService(Type type) =>
        type == typeof(IServiceProviderIsKeyedService) ? null : provider.GetService(type);
    public object? GetKeyedService(Type type, object? key) =>
        ((IKeyedServiceProvider)provider).GetKeyedService(type, key);
    public object GetRequiredKeyedService(Type type, object? key) =>
        ((IKeyedServiceProvider)provider).GetRequiredKeyedService(type, key);
}

Run dotnet run --project artifacts/release-review-20261009/issue-probes/keyed-probe/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

Registration Current source Published 0.8.0
Ordinary unkeyed type Work Work
Same type with forwarding decorator InvalidOperationException: No service for type 'Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService' has been registered. Wrapper

Expected: ordinary unkeyed decoration retains its previous provider capability requirements while preserving the corrected native constructor rules. No keyed availability check is necessary for this parameterless original.

Fix direction and regression coverage

Require the keyed availability probe only when keyed lookup/planning actually needs it, with an appropriate path for ordinary activation. Preserve the #111 constructor-selection fixes and the existing mapped/keyed provider contracts. Cover parameterless and ordinary-dependency originals, provider wrappers lacking the keyed probe, repeated decorators and disposal; retain explicit-key and inherited-key controls.

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.

Related: closed #111. This new report isolates the additional provider-capability requirement introduced in original decorator activation.

Prepared with OpenAI Codex.

Activity

  1. AGiorgetti commented on Oct 9, 2026

    @AGiorgetti
    ContributorAuthor

    Implemented in draft PR #123: #123

    This was prioritized as the new regression among the open bug/P2 reports. The exact issue reproduction independently confirms that develop fails after decoration on net472/net8.0/net9.0/net10.0 while published 0.8.0 succeeds on every target. The fixed source now succeeds on every target too.

    The original-type activator previously requested IServiceProviderIsKeyedService before examining any constructor. It now acquires that probe only when a constructor dependency needs a non-null lookup key. Parameterless and ordinary-dependency originals, null owner contexts, and direct ServiceKey injection therefore retain ordinary provider support. Private decorator slots still use keyed resolution; availability probing and keyed resolution remain separate capabilities. Actual explicit/inherited-key dependencies still fail clearly when the keyed availability probe is missing. Nonempty DependsOn maps retain their existing requirements.

    Preserving #111's generic-constraint checks required the existing snapshot helper/cache to accept the ordinary probe interface as well. For native DI, both interfaces refer to the same CallSiteFactory descriptor owner. The helper keeps its exact native-type guard, copied descriptor field, metadata-shape guard, weak provider-specific cache, and preference for Mammoth's snapshot. There is no additional reflection lookup or dependency activation during planning. The rationale is documented in the architecture document, already linked from the code comment and README Architecture section.

    Tests were committed first: 75 failed and 6 keyed controls passed on each target. All 81 new cases now pass, including wrappers whose ordinary probe implements only IServiceProviderIsService, native/snapshot/diagnostic providers, repeated layers, lifetimes/disposal, constructor policies, constraint failures, and keyed controls.

    Verification for final commit 7cb718572fa423b050c6083ae554f66064d35cb6:

    • Local Windows restore and Release CI-style build passed.
    • Local and Windows CI suites each passed 2,578 tests on all four targets: 10,312 passed, zero failed/skipped per run. net472 executed under .NET Framework 4.8.9345.0.
    • Local and CI native DI 10.0.0 cache probes passed on all four targets: 100 interleaved enumerations each after observed native compilation.
    • Whitespace check passed. No applicable checks were unavailable.
    • Only the two existing net472 support warnings from Microsoft.Extensions.Telemetry.Abstractions and Microsoft.Extensions.Diagnostics.Testing 10.0.0 remain; no compiler/analyzer warnings or errors.
    • Exact-commit CI succeeded. Pack and Publish were skipped.

    Paused for review before another issue. #121 and #122 remain separate open defects; resolve and review those before completing the release-validation checkpoint. 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