Repository navigation
Unkeyed decoration regresses providers without a keyed availability probe #120
Description
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
AGiorgetti commentedon Oct 9, 2026
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: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.
test: reproduce ordinary decoration keyed-probe regression (#120)
fix: require keyed availability only for keyed original dependencies (#…