Skip to content

[Bug]: Source Generator Incorrectly Passes Contract Name to Constructor Dependency Resolution #305

@Soar360

Description

@Soar360

Describe the bug 🐞

When using Splat.DI.SourceGenerator to register a type with constructor dependencies, if the parent type is registered with a contract name, the source generator incorrectly passes that contract name to the dependency resolution of constructor parameters, resulting in a runtime InvalidOperationException.

Step to reproduce

  1. Define the following classes:
public class S1 { static S1() { Splat.SplatRegistrations.Register<S1>(); } }
public class ViewBase { }
public class V1 : ViewBase { static V1() { Splat.SplatRegistrations.Register<ViewBase, V1>("V1"); }
public V1(S1 s1) { }
}
  1. Call the generated registration method:
SplatRegistrations.SetupIOC(); 
var v1 = Locator.Current.GetService<ViewBase>("V1");
  1. The generated code incorrectly uses the parent type's contract name to resolve constructor dependencies.
  2. System.InvalidOperationException: Dependency 'global::S1' with contract V1 not registered with Splat resolver.

Reproduction repository

https://github.com/reactiveui/ReactiveUI

Expected behavior

The generated code should correctly resolve constructor dependencies without a contract name.

Screenshots 🖼️

No response

IDE

No response

Operating system

No response

Version

No response

Device

No response

ReactiveUI Version

No response

Additional information ℹ️

Environment

  • .NET Version: .NET 10
  • Splat.DI.SourceGenerator Version: [Please specify your version]
  • IDE: Visual Studio 2026

Root Cause

The source generator incorrectly inherits the parent type's contract name when analyzing constructor parameter dependencies. Constructor parameter dependency resolution should be based on the parameter type's own registration information, not the parent type's contract name.

Suggested Fix

When generating constructor dependency resolution code, the generator should:

  1. Check if the dependency type (e.g., S1) has its own contract name registration
  2. If the dependency type was registered without a contract name, call GetService<T>() without parameters
  3. If the dependency type was registered with a contract name, use that contract name in GetService<T>(contract)
  4. Never pass the parent type's contract name to constructor parameter dependency resolution

Additional Context

Complete generated code example:

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

// Suppress warnings about [Obsolete] member usage in generated code.
#pragma warning disable CS0612, CS0618

namespace Splat
{
    /// <summary>
    /// Provides extension methods for the Splat dependency injection source generator.
    /// This partial class contains the generated implementation of dependency registrations.
    /// </summary>
    internal static partial class SplatRegistrations
    {
        /// <summary>
        /// Internal implementation of dependency injection setup.
        /// This method is generated at compile-time and contains all registrations
        /// defined through the source generator marker methods.
        /// </summary>
        /// <param name="resolver">The <see cref="Splat.IDependencyResolver"/> instance to register dependencies with.</param>
        static partial void SetupIOCInternal(Splat.IDependencyResolver resolver)
        {
            resolver.Register<global::S1>(() => new global::S1());

            resolver.Register<global::ViewBase>(() => new global::V1(resolver.GetService<global::S1>("V1") ?? throw new global::System.InvalidOperationException("Dependency 'global::S1' with contract " + "V1" + " not registered with Splat resolver.")), "V1");
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions