Extenject Documentation

repository·master·Indexed 20 days ago

https://github.com/mathijs-bakker/extenject

A high-performance dependency injection framework for Unity 3D and standard C# applications, provided as an actively maintained fork of Zenject. Extenject promotes loose coupling and testability through features such as constructor, field, property, and method injection, reflection baking to eliminate overhead, and support for various Unity platforms including iOS, Android, and WebGL. It includes tools for object lifecycle management, sub-containers, and object graph validation.

Tokens
47.6K
Snippets
111
Records
177
Agent score
66%

What's inside Extenject

  1. What is Extenject?

    master
    Extenject is a lightweight, highly performant dependency injection (DI) framework specifically designed for Unity 3D, though it can also be used in standard C# environments. It allows you to decouple application components, making code more reusable, refactorable, and testable by managing how objects are instantiated and how their dependencies are provided.
  2. Karma: MVCP Framework for Unity3D

    master

    Karma is an MVC (Model-View-Controller-Presenter) framework specifically designed for Unity3D. Due to Unity's architecture, it implements an MVCP pattern where the Presenter is a MonoBehaviour acting as an intermediary between the Controller and the View.

    Karma is built on top of Zenject to leverage Dependency Injection (DI). In Karma, DI is primarily used to:

    • Route the application to the desired view.
    • Enable the construction of composable and testable systems.

    The framework's constructs and layout are inspired by web frameworks like AngularJS and ASP vNext.

  3. EcsRx: Reactive ECS Framework

    master

    EcsRx is a reactive implementation of the Entity Component System (ECS) pattern. It is designed for .NET applications and games, emphasizing Inversion of Control (IoC) and composition over inheritance. It is suitable for developers looking for a lightweight, reactive architecture with built-in support for events, pooling, and plugins.

    Key Features:

    • Reactive Architecture: Fully reactive design using Rx.
    • IoC Adherence: Follows Inversion of Control principles.
    • Event Support: Built-in mechanisms to raise and react to events.
    • Pooling Support: Built-in support for object pooling, with the ability to wrap 3rd party tools.
    • Plugin System: Support for wrapping and sharing components, systems, and events.

    Note: While the core framework is for .NET, a Unity-specific version exists at ecsrx/ecsrx.unity.

  4. What is Dependency Injection (DI)?

    master

    Dependency Injection is a design pattern used to achieve loose coupling between classes. Instead of a class creating its own dependencies (which leads to tight coupling), dependencies are "injected" into the class, typically via its constructor.

    The Problem: Tight Coupling

    When a class instantiates its own dependencies, it becomes hard to change implementations without modifying the class itself:

    public class Foo
    {
        ISomeService _service;
    
        public Foo()
        {
            _service = new SomeService(); // Tight coupling to concrete SomeService
        }
    }

    The Solution: Dependency Injection

    By passing the dependency through the constructor, the class only cares about the interface, not the concrete implementation:

    public class Foo
    {
        ISomeService _service;
    
        public Foo(ISomeService service)
        {
            _service = service;
        }
    }

    The Composition Root

    To avoid passing dependencies manually through every layer of your application (the "prop drilling" problem), DI frameworks like Extenject automate the process. The responsibility of deciding which concrete implementations to use is moved to a single location called the Composition Root. Extenject automates the creation and wiring of this object graph.

    public class Foo
    {
        ISomeService _service;
    
        public Foo(ISomeService service)
        {
            _service = service;
        }
    }
  5. What is Mono.Cecil and how can it be used?

    master

    Mono.Cecil is a library used to generate and inspect programs and libraries in the ECMA CIL (Common Intermediate Language) form. It allows you to perform two primary tasks without needing to load assemblies via Reflection:

    1. Analyze .NET binaries: Use a powerful object model to inspect existing binaries.
    2. Modify .NET binaries: Add new metadata structures and alter the IL (Intermediate Language) code directly.

    Because it does not require loading assemblies into the execution context, it is a lightweight way to perform deep inspection and manipulation of .NET code.

  6. How to correctly handle external instances with QueueForInject

    master

    When you have an instance that exists at startup but was not created by Zenject (e.g., var foo = new Foo();), you should not call Container.Inject(foo) immediately during the install phase. Doing so can cause errors if foo depends on bindings that haven't been registered yet.

    Instead, use QueueForInject. This tells Zenject to inject the instance during the initial object graph construction, immediately after the install phase. This ensures that all dependencies are ready and that Zenject can guarantee the correct injection order for dependent classes.

    var foo = new Foo();
    
    // Correct way: Bind the instance and queue it for injection
    Container.BindInstance(foo);
    Container.QueueForInject(foo);
  7. Manage destruction and disposal order

    master

    Extenject allows you to control the disposal order of classes implementing IDisposable via execution order settings. However, Unity does not guarantee a deterministic destruction order for GameObjects or scenes during application quit.

    Ensuring predictable destruction for GameObjects

    To ensure IDisposable bindings are destroyed before scene GameObjects, place your objects as children of the SceneContext. You can enable the Parent New Objects Under Scene Context setting on the SceneContext component to automatically parent all dynamically instantiated objects under the context.

    Ensuring deterministic scene destruction on quit

    By default, Unity's destruction order for scenes and DontDestroyOnLoad objects (including ProjectContext) is non-deterministic. To force a sensible order, set the Zenject setting Ensure Deterministic Destruction Order On Application Quit to true.

    When enabled, scenes are destroyed in the reverse order they were loaded, followed by the destruction of DontDestroyOnLoad objects.

    Warning: This setting is disabled by default because it can cause crashes on Android.

  8. Benefits of using Extenject

    master

    Using a DI framework like Extenject provides several architectural advantages:

    • Single Responsibility Principle: Classes focus on their specific logic rather than the mechanics of wiring up dependencies.
    • Refactorability: Loose coupling makes the codebase resilient to changes; you can swap implementations without affecting dependent classes.
    • Modular Code: Forces developers to think clearly about the interfaces between different modules.
    • Testability: Makes automated unit testing easy by allowing you to create a different "composition root" that injects mock or stub implementations instead of real services.
  9. Implement Abstract Factories for interfaces

    master

    An Abstract Factory is used when you want a factory to return an interface rather than a concrete class. This allows you to swap implementations at runtime or during installation without changing the consumer code.

    To implement this:

    1. Define an interface (e.g., IPathFindingStrategy).
    2. Create a factory class inheriting from PlaceholderFactory<IInterface>.
    3. In your installer, use Container.BindFactory<IInterface, YourFactory>().To<ConcreteImplementation>().

    Example

    public interface IPathFindingStrategy { /* ... */ }
    public class AStarPathFindingStrategy : IPathFindingStrategy { /* ... */ }
    
    public class PathFindingStrategyFactory : PlaceholderFactory<IPathFindingStrategy> { }
    
    // In the Installer
    public override void InstallBindings()
    {
        if (UseAStar)
        {
            Container.BindFactory<IPathFindingStrategy, PathFindingStrategyFactory>().To<AStarPathFindingStrategy>();
        }
        else
        {
            Container.BindFactory<IPathFindingStrategy, PathFindingStrategyFactory>().To<RandomPathFindingStrategy>();
        }
    }
    Container.BindFactory<IPathFindingStrategy, PathFindingStrategyFactory>().To<AStarPathFindingStrategy>();
  10. Use Abstract Signals with Interfaces to reduce coupling

    master

    To avoid coupling subscribers to concrete signal types, you can use Abstract Signals. Instead of subscribing to a specific struct like SignalLevelCompleted, you can have multiple signal structs implement a common interface (e.g., ISignalGameSaver) and subscribe to that interface instead.

    Key API differences:

    • Declaration: Use Container.DeclareSignalWithInterfaces<T>() instead of DeclareSignal<T>(). This registers both the concrete type and its implemented interfaces.
    • Firing: Use signalBus.AbstractFire<T>() instead of signalBus.Fire<T>(). AbstractFire will fire the signal via its interfaces. If the signal was not declared with interfaces using DeclareSignalWithInterfaces, it will throw an exception.

    This allows a system (like a SoundSystem) to react to any signal that implements a specific interface (like ISignalSoundPlayer) without needing to know the specific event that triggered it.

    // 1. Define interfaces for capabilities
    public interface ISignalGameSaver {}
    public interface ISignalSoundPlayer { int SoundId { get; }
    }
    
    // 2. Implement interfaces in concrete signal structs
    public struct SignalCheckpointReached : ISignalGameSaver, ISignalSoundPlayer
    {
        public int SoundId => 2;
    }
    
    // 3. In your Installer, declare with interfaces
    Container.DeclareSignalWithInterfaces<SignalCheckpointReached>();
    
    // 4. In your System, subscribe to the interface
    public class SaveGameSystem
    {
        public SaveGameSystem(SignalBus signalBus)
        {
            signalBus.Subscribe<ISignalGameSaver>(x => SaveGame());
        }
        void SaveGame() { /* ... */ }
    }
    
    // 5. Fire using AbstractFire
    public class Example
    {
        SignalBus signalBus;
        public void CheckpointReached() => signalBus.AbstractFire<SignalCheckpointReached>();
    }
  11. How Signals work for loose coupling

    master

    Signals provide a way for two classes to communicate without being strongly coupled. Instead of Class A calling a method on Class B (strong coupling) or Class B observing an event on Class A (inverse strong coupling), both classes interact with an intermediary: the SignalBus.

    This allows for a 'fire and forget' pattern where the sender does not need to know who the receivers are, or if anyone is listening at all. This is ideal for high-level, game-wide events, but should be used cautiously to avoid 'callback hell' where complex event chains make the system hard to follow.

    // Conceptual flow:
    // Sender -> SignalBus.Fire(new MySignal()) -> SignalBus -> Receiver.OnSignal(MySignal signal)