Extenject Documentation
repository·master·Indexed 20 days ago
https://github.com/mathijs-bakker/extenjectA 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.
What's inside Extenject
- 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.
Karma: MVCP Framework for Unity3D
masterKarma 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
MonoBehaviouracting 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.
EcsRx: Reactive ECS Framework
masterEcsRx 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.What is Dependency Injection (DI)?
masterDependency 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; } }What is Mono.Cecil and how can it be used?
masterMono.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:
- Analyze .NET binaries: Use a powerful object model to inspect existing binaries.
- 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.
How to correctly handle external instances with QueueForInject
masterWhen you have an instance that exists at startup but was not created by Zenject (e.g.,
var foo = new Foo();), you should not callContainer.Inject(foo)immediately during the install phase. Doing so can cause errors iffoodepends 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);Manage destruction and disposal order
masterExtenject allows you to control the disposal order of classes implementing
IDisposablevia execution order settings. However, Unity does not guarantee a deterministic destruction order forGameObjectsor scenes during application quit.Ensuring predictable destruction for GameObjects
To ensure
IDisposablebindings are destroyed before scene GameObjects, place your objects as children of theSceneContext. You can enable theParent New Objects Under Scene Contextsetting on theSceneContextcomponent 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
DontDestroyOnLoadobjects (includingProjectContext) is non-deterministic. To force a sensible order, set the Zenject settingEnsure Deterministic Destruction Order On Application Quittotrue.When enabled, scenes are destroyed in the reverse order they were loaded, followed by the destruction of
DontDestroyOnLoadobjects.Warning: This setting is disabled by default because it can cause crashes on Android.
Benefits of using Extenject
masterUsing 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.
Implement Abstract Factories for interfaces
masterAn 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:
- Define an interface (e.g.,
IPathFindingStrategy). - Create a factory class inheriting from
PlaceholderFactory<IInterface>. - 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>();- Define an interface (e.g.,
Use Abstract Signals with Interfaces to reduce coupling
masterTo 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 ofDeclareSignal<T>(). This registers both the concrete type and its implemented interfaces. - Firing: Use
signalBus.AbstractFire<T>()instead ofsignalBus.Fire<T>().AbstractFirewill fire the signal via its interfaces. If the signal was not declared with interfaces usingDeclareSignalWithInterfaces, it will throw an exception.
This allows a system (like a
SoundSystem) to react to any signal that implements a specific interface (likeISignalSoundPlayer) 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>(); }- Declaration: Use
How Signals work for loose coupling
masterSignals 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)Use Memory Pools for object pooling
masterExtenject (v5.0+) supports Memory Pools, providing a fluent interface for managing object pools. This allows for efficient reuse of objects instead of constant allocation and garbage collection.