Skip to content

DependsOn rejects registered null factory results for nullable constructor dependencies #110

Closed

Description

@AGiorgetti

Priority: P2

Problem

Constructor selection accepts a registered dependency, but Resolve uses GetRequiredService/GetRequiredKeyedService. When an existing factory returns null, these methods throw instead of supplying the result to a nullable constructor parameter. Standard DI accepts the same registration and injects null.

Distinct from closed #83 (exception unwrapping) and #88 (ordinary diagnostic activation); the failure remains in mapped activation.

Source: ConstructorActivator.cs:128.

Reproduction

Create artifacts/review-null-factory/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 mapped in new[] { false, true })
    Report(mapped ? "mapped" : "native", () => {
        var s = new ServiceCollection();
        s.AddTransient<Part>(_ => null!); s.AddSingleton("configured");
        if (mapped) s.AddTransient<Consumer>([Dependency.OnValue("label", "configured")]);
        else s.AddTransient<Consumer>();
        using var p = s.BuildServiceProvider();
        Console.WriteLine(p.GetRequiredService<Consumer>().Part == null);
    });
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 sealed class Part { }
public sealed class Consumer(string label, Part? part = null) { public Part? Part { get; } = part; }

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

Observed versus expected

Native activation succeeds and prints True. Adding an unrelated label map makes Mammoth throw No service for type 'Part' has been registered. The Part factory is registered and returns null. The same divergence exists in published 0.8.0.

Preserve results from registered ordinary/attribute-keyed factories, including null, when supplying nullable constructor dependencies. Optional defaults must apply to missing registrations rather than a registered factory's null result. Keep genuine missing required registrations and application-thrown exceptions intact.

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 #110 in PR #117, awaiting review at commit f5e4c5351416db4fe446c568d50088830ebf48d9.

    Constructor selection already checks whether ordinary, attribute-keyed and named-key dependencies are registered. Required-service lookups then incorrectly treated a registered factory's null result as a missing registration. The fix uses GetService/GetKeyedService after those availability checks, preserving null for constructor injection. Optional defaults still apply only when registrations are missing; missing required dependencies still fail before activating factories; application exceptions propagate with their original identity. The shared path also covers contextual keyed decorators. An explanatory source comment and vNext changelog entry are included.

    Regression tests were added first and verified against pre-fix code on net472/net8.0/net9.0/net10.0: 84 failed and 30 controls passed per target. The 114 new cases cover nullable references and nullable values with non-null defaults, all key modes, all dependency lifetimes, native/snapshot/diagnostic providers, repeated resolutions across scopes, decoration, missing dependencies and factory exceptions.

    Final local Release restore/build/full tests passed: 1,978 tests per target, 7,912 total, zero failures/skips. All four native hot-cache probes passed 100 interleaved enumerations after native compilation. Windows net472 ran on .NET Framework 4.8.9345.0. The build has only the two existing net472 support warnings for Telemetry.Abstractions and Diagnostics.Testing 10.0.0; no compiler/analyzer warnings. No applicable check was unavailable.

    CI run 37794439913 passed for the exact final commit; downloaded logs confirm those test totals and all four native probes. Pack and Publish were skipped.

    The PR documents an upstream caching nuance: native constructor call sites can retry null-returning singleton factories, while public service accessors cache null. Mammoth retains public lookup caching; scoped/transient factory-call counts match native controls.

    Stopping here for your review and explicit instruction to continue.

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