AutoInject for Godot C#

repository·main·Indexed 42 days ago

https://github.com/chickensoft-games/autoinject

A reflection-free, node-based dependency injection system for C# Godot scripts. It enables nodes to provide dependencies to descendants via the scene tree using mixins like IProvider and IDependent, and includes Roslyn analyzers for diagnostics and code fixes.

Tokens
4.8K
Snippets
12
Records
15
Agent score
4%

What's inside AutoInject

  1. Use AutoInject Analyzers for Godot node scripts

    main

    AutoInject Analyzers are Roslyn analyzers that provide diagnostics and code fixes for common issues when using Chickensoft.AutoInject within Godot node scripts. They help ensure that dependency injection patterns are correctly implemented and that required lifecycle methods are called.

    Supported diagnostics and fixes include:

    • Missing override of void _Notification(int what): Ensures the node correctly overrides the notification method required for the injection lifecycle.
    • Missing call to this.Notify(): Ensures that nodes correctly trigger notifications to update dependencies.
    • Missing call to this.Provide() for nodes implementing IProvider: Ensures that nodes providing dependencies correctly call Provide() to register them in the injection tree.
  2. How AutoInject dependency resolution works

    main

    AutoInject resolves dependencies by inspecting nodes for the [Dependency] attribute and searching up the scene tree.

    The Resolution Lifecycle:

    1. Introspection: The Introspection generator creates metadata so the node can inspect its own properties without runtime reflection.
    2. Trigger: When a node receives the Node.NotificationReady signal, it automatically begins resolution.
    3. Search: The node searches its ancestors (starting with itself) for nodes implementing IProvideAny or IProvide<T>.
    4. Synchronization: A dependent node's OnResolved is only called once all its required dependencies have been found and their providers have initialized.

    The this.Provide() Convention: To ensure a predictable execution order and avoid deadlocks, providers should call this.Provide() inside their _Ready method. This guarantees the following synchronous order:

    1. Dependent node _Ready (deepest nodes first).
    2. Provider node _Ready (calls Provide).
    3. Dependent OnResolved.
    4. Frame 1 _Process.

    Tip: If a provider is also a dependent, call this.Provide() from its OnResolved() to allow it to provide dependencies to its own subtree safely.

  3. Understand the Enhanced Lifecycle in AutoInject

    main

    AutoInject extends the standard Godot node lifecycle to provide more controlled hooks for dependency handling. The lifecycle depends on whether the node is running in a game environment or a test environment.

    Game Environment Lifecycle: Initialize() $\rightarrow$ OnReady() $\rightarrow$ Setup() $\rightarrow$ OnResolved()

    Test Environment Lifecycle: OnReady() $\rightarrow$ OnResolved()

    This lifecycle is preserved regardless of how the node is added to the scene tree.

  4. How AutoInject mixins work

    main

    AutoInject uses the Introspection generator to add mixins to C# classes. These mixins allow nodes to gain state and handle lifecycle events without reflection.

    CRITICAL REQUIREMENT: For any mixin to work, you must override _Notification in your node script and call this.Notify(what). This allows the mixins to intercept Godot notifications and invoke their handler methods.

    Available Mixins:

    • IAutoOn: Implements .NET-style handlers for Godot notifications (e.g., OnReady, OnProcess).
    • IAutoConnect: Automatically binds properties marked with [Node] to scene tree nodes.
    • IAutoInit: Adds an initialization lifecycle method called before _Ready (if IsTesting is false).
    • IProvider: Allows a node to provide dependencies to descendants.
    • IDependent: Allows a node to request dependencies from ancestors.
    • IAutoNode: A convenience mixin that applies all the above mixins at once.
    using Chickensoft.AutoInject;
    using Chickensoft.Introspection;
    using Godot;
    
    // Apply all AutoInject functionality at once
    [Meta(typeof(IAutoNode))]
    public partial class MyNode : Node
    {
      // REQUIRED for mixins to function
      public override void _Notification(int what) => this.Notify(what);
    }
  5. Test IAutoConnect with a fake node tree

    main

    You can test nodes using IAutoConnect by substituting the real scene tree with a fake one using FakeNodeTree.

    Important Requirement: When using GodotNodeInterfaces v3 (AutoInject 2.14.5+), you must set the static property Chickensoft.GodotNodeInterfaces.RuntimeContext.IsTesting = true when running tests to enable the fake node tree functionality.

    [Test]
    public void UsesFakeNodeTree()
    {
      // Manually trigger instantiation signal for non-scene-instantiated nodes
      _scene._Notification((int)Node.NotificationSceneInstantiated);
    
      _scene.SomeNode.ShouldBe(_someNode.Object);
      _scene.MyUniqueNode.ShouldBe(_myUniqueNode.Object);
      _scene.DifferentName.ShouldBe(_otherUniqueNode.Object);
    }
  6. Subscribe to dependency changes instead of parent events

    main

    To avoid complex dependency hierarchies and ensure robust logic, subscribe to events emitted by the dependency values themselves rather than subscribing to a parent node's events. This ensures your logic reacts directly to the data it depends on.

    Best Practices:

    • Use OnResolved to set up subscriptions once dependencies are valid.
    • Always clean up subscriptions in OnExitTree to prevent memory leaks.
    [Meta(typeof(IAutoNode))]
    public partial class MyDependent : Node
    {
      public override void _Notification(int what) => this.Notify(what);
    
      [Dependency]
      public MyValue Value => this.DependOn<MyValue>();
    
      public void OnResolved()
      {
        // Setup subscriptions once dependencies are valid.
        MyValue.OnSomeEvent += ValueUpdated
      }
    
      public void OnExitTree()
      {
        // Clean up subscriptions here!
        MyValue.OnSomeEvent -= ValueUpdated
      }
    
      public void ValueUpdated()
      {
        // Do something in response to the value we depend on changing.
      }
    }
  7. Install AutoInject in a Godot C# project

    main

    AutoInject is a source-only package that requires the Introspection source generator. To install it, add the following packages to your .csproj file. Ensure you specify the correct versions from NuGet.

    Required packages:

    • Chickensoft.GodotNodeInterfaces
    • Chickensoft.Introspection
    • Chickensoft.Introspection.Generator (set as an analyzer)
    • Chickensoft.AutoInject (set as an analyzer)

    It is highly recommended to treat warning CS9057 as an error to catch compiler-mismatch issues with the Introspection generator.

    <ItemGroup>
        <PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="..." />
        <PackageReference Include="Chickensoft.Introspection" Version="..." />
        <PackageReference Include="Chickensoft.Introspection.Generator" Version="..." PrivateAssets="all" OutputItemType="analyzer" />
        <PackageReference Include="Chickensoft.AutoInject" Version="..." PrivateAssets="all" />
    </ItemGroup>
    
    <!-- Optional: Add analyzers for extra checks and code fixes -->
    <ItemGroup>
        <PackageReference Include="Chickensoft.AutoInject.Analyzers" Version="..." PrivateAssets="all" OutputItemType="analyzer" />
    </ItemGroup>
    
    <!-- Recommended: Catch compiler-mismatch issues -->
    <PropertyGroup>
      <TargetFramework>net8.0</TargetFramework>
      <WarningsAsErrors>CS9057</WarningsAsErrors>
    </PropertyGroup>
  8. Provide fallback values for dependencies

    main

    You can provide a fallback value to a dependency using a lambda expression. This is useful for running scenes in isolation (e.g., in the editor) without needing to set up the full production dependency tree. The fallback is only used if no provider is found in the ancestor nodes.

    [Dependency]
    public string MyDependency => this.DependOn<string>(() => "fallback_value");
  9. Use IAutoInit to enable two-phase initialization for testing

    main

    The IAutoInit mixin allows you to split a Godot node's initialization into two phases: Initialize() and OnReady(). This is designed to make unit testing easier by allowing you to mock dependencies before the node's logic runs.

    • Initialize(): Used for late member initialization (e.g., assigning concrete implementations to dependencies). In a real game environment, this is called automatically from _Ready.
    • OnReady(): Guaranteed to be called after Initialize(). This is where you use the dependencies set up in the previous step.

    Testing Behavior: In a test environment, Initialize() is skipped if the IsTesting property is set to true. This prevents the node from overwriting your mocked dependencies with real ones.

    using Chickensoft.AutoInject;
    using Chickensoft.Introspection;
    using Godot;
    
    [Meta(typeof(IAutoInit), typeof(IAutoOn))]
    public partial class MyNode : Node2D
    {
      public override void _Notification(int what) => this.Notify(what);
    
      public IMyObject Obj { get; set; } = default!;
    
      public void Initialize()
      {
        // Called from _Ready if IsTesting is false.
        // Initialize values which would be mocked in a unit testing method.
        Obj = new MyObject();
      }
    
      public void OnReady()
      {
        // Guaranteed to be called after Initialize()
        // Uses the object setup in Initialize() (or the mock supplied by a test)
        Obj.DoSomething();
      }
    }

    How to use in a Unit Test:

    var myNode = new MyNode()
    {
      Obj = mock.Object
    };
    
    // Prevent Initialize() from overwriting the mock
    (myNode as IAutoInit).IsTesting = true;
  10. Create a Provider to supply dependencies

    main

    To make a value available to descendant nodes, implement the IProvider mixin and the IProvide<T> interface for each type you wish to provide.

    Workflow:

    1. Add [Meta(typeof(IProvider))] (or IAutoNode) to your class.
    2. Implement IProvide<T>.Value() for each dependency.
    3. Crucially, call this.Provide() once your dependencies are initialized (e.g., in OnReady or _Ready). This informs AutoInject that the values are ready for consumption.

    Note: You can optionally implement OnProvided() to run logic immediately after this.Provide() is called.

    namespace MyGameProject;
    
    using Chickensoft.AutoInject;
    using Chickensoft.Introspection;
    using Godot;
    
    [Meta(typeof(IAutoNode))]
    public partial class MyProvider : Node, IProvide<string>
    {
      public override void _Notification(int what) => this.Notify(what);
    
      // Implementation of the dependency
      string IProvide<string>.Value() => "Value";
    
      // Inform AutoInject that the value is ready
      public void OnReady() => this.Provide();
    
      public void OnProvided()
      {
        // Optional: logic after providing
      }
    }
  11. Apply all AutoInject mixins at once with IAutoNode

    main

    If you want to apply all the mixins provided by AutoInject to a node script simultaneously, use the IAutoNode mixin.

    using Chickensoft.AutoInject;
    using Chickensoft.Introspection;
    using Godot;
    
    [Meta(typeof(IAutoNode))]
    public partial class MyNode : Node { }
  12. Use IAutoOn to implement .NET-style Godot notification handlers

    main

    The IAutoOn mixin allows you to implement Godot notifications using standard .NET-style method names prefixed with On.

    Important Note on Processing: Unlike standard Godot overrides (like _Process or _PhysicsProcess), using OnProcess or OnPhysicsProcess does not automatically enable the node's processing. You must manually enable them:

    • For OnProcess, call SetProcess(true);
    • For OnPhysicsProcess, call SetPhysicsProcess(true);
    using Chickensoft.AutoInject;
    using Chickensoft.Introspection;
    using Godot;
    
    [Meta(typeof(IAutoOn))]
    public partial class MyNode : Node2D
    {
      public override void _Notification(int what) => this.Notify(what);
    
      public void OnReady() {
        // Called when the node enters the scene tree.
        SetProcess(true);
      }
    
      public void OnProcess(double delta) {
        // Called every frame.
      }
    }