Zenject
Unity3D
Editor
Scene
GameObject
SceneContext.cs
Installers
Scriptable
.asset
Mono
.cs
Prefabs
.gameObject
Edit -> Zenject -> Validate Current Scene
Library
Injection
Constructor
Field
Property
Method
Inject methods are the recommended approach for MonoBehaviours, since MonoBehaviours cannot have constructors.
There can be any number of inject methods. In this case, they are called in the order of Base class to Derived class
This can be useful to avoid the need to forward many dependencies from derived classes to the base class via constructor parameters, while also guaranteeing that the base class inject methods complete first, just like how constructors work.
Inject methods are called after all other injection types
It is designed this way so that these methods can be used to execute initialization logic which might make use of injected fields or properties.
Note also that you can leave the parameter list empty if you just want to do some initialization logic only.
Notes
With the exception where there is circular dependencies
You can safely assume that the dependencies that you receive via inject methods will themselves already have been injected
This can be important if you use inject methods to perform some basic initialization, since in that case you may need the given dependencies to be initialized as well
Note however that it is usually not a good idea to use inject methods for initialization logic
Often it is better to use IInitializable.Initialize or Start() methods instead, since this will allow the entire initial object graph to be created first.
Recommendations
Best practice is to prefer constructor/method injection compared to field/property injection
Resolve at initialization
Constructor injection forces the dependency to only be resolved once, at class creation, which is usually what you want. In most cases you don't want to expose a public property for your initial dependencies because this suggests that it's open to changing
No circular dependencies
Constructor injection guarantees no circular dependencies between classes, which is generally a bad thing to do
Zenject does allow circular dependencies when using other injections types however such as method/field/property injection
// When would you want his????
Simpler to port
Constructor/Method injection is more portable for cases where you decide to re-use the code without a DI framework such as Zenject
You can do the same with public properties but it's more error prone (it's easier to forget to initialize one field and leave the object in an invalid state)
Clear dependencies
Finally, Constructor/Method injection makes it clear what all the dependencies of a class are when another programmer is reading the code
They can simply look at the parameter list of the method
his is also good because it will be more obvious when a class has too many dependencies and should therefore be split up (since its constructor parameter list will start to seem long)
Binding
Every dependency injection framework is ultimately just a framework to bind types to instances
DiContainer
In Zenject, dependency mapping is done by adding bindings to something called a container
The container should then 'know' how to create all the object instances in your application, by recursively resolving all dependencies for a given object.
C# Reflection
[Inject]
When the container is asked to construct an instance of a given type, it uses C# reflection to find the list of constructor arguments, and all fields/properties that are marked with an [Inject] attribute
It then attempts to resolve each of these required dependencies, which it uses to call the constructor and create the new instance.
C# Mono Example
Class
public class Foo
{
IBar _bar;
public Foo(IBar bar)
{
_bar = bar;
}
}
Bind
Instance
Container.Bind<Foo>().AsSingle();
AsSingle
This tells Zenject that every class that requires a dependency of type Foo should use the same instance, which it will automatically create when needed
InterfaceToInstance
Container.Bind<IBar>().To<Bar>().AsSingle();
<IBar().To<Bar>
Any class that requires the IBar interface (like Foo) will be given the same instance of type Bar.
Full Bind
Container.Bind<ContractType>()
.WithId(Identifier)
.To<ResultType>()
.FromConstructionMethod()
.AsScope()
.WithArguments(Arguments)
.OnInstantiated(InstantiatedCallback)
.When(Condition)
.(Copy|Move)Into(All|Direct)SubContainers()
.NonLazy()
.IfNotBound();
ContractType
ResultType
Identifier
ConstructionMethod
Scope
Arguments
InstantiatedCallback
Condition
(Copy|Move)Into(All|Direct)SubContainers
NonLazy
IfNotBound
Construction Methods
Installers
C# Native Examples
ITickable
IInitializable
IDisposable
BindInterfacesTo
BindInterfacesAndSelfTo
Object Graph Validation
Scene Bindings
General Guidelines / Recommendations / Gotchas / Tips and Tricks
Runtime Parameters For Installers
Using Zenject Outside Unity Or For DLLs
Zenject Settings
Creating Objects Dynamically Using Factories
Ensure that new dynamic object gets injected with dependencies just like all the objects that are part of the initial object graph
MonoBehaviours
Note that for dynamically instantiated MonoBehaviours (for example when using FromComponentInNewPrefab with BindFactory) injection should always occur before Awake and Start, so a common convention we recommend is to use Awake/Start for initialization logic and use the inject method strictly for saving dependencies (ie. similar to constructors for non-monobehaviours)
Theory
Important part of dependency injection is to reserve use of the container to strictly the "Composition Root Layer"
factories and installers make up what we refer to as the "composition root layer"
Example
anti-pattern / Service Locator Pattern
Target
Recommended Zenject
public class Player{}
public class Enemy{
readonly Player _player;
public Enemy(Player player){
_player = player;
}
public class Factory : PlaceholderFactory<Enemy>{}
}
public class EnemySpawner : ITickable{
readonly Enemy.Factory _enemyFactory;
public EnemySpawner(Enemy.Factory enemyFactory){
_enemyFactory = enemyFactory;
}
public void Tick(){
if (ShouldSpawnNewEnemy()){
var enemy = _enemyFactory.Create();
// ...
}
}
}
public class TestInstaller : MonoInstaller{
public override void InstallBindings(){
Container.BindInterfacesTo<EnemySpawner>().AsSingle();
Container.Bind<Player>().AsSingle();
Container.BindFactory<Enemy, Enemy.Factory>();
}
}
By using Enemy.Factory above instead of new Enemy, all the dependencies for the Enemy class (such as the Player) will be automatically filled in.
We can also add runtime parameters to our factory. For example, let's say we want to randomize the speed of each Enemy to add some interesting variation to our game. Our enemy class becomes:
public class Factory : PlaceholderFactory<float, Enemy>{}
from
public class Factory : PlaceholderFactory< Enemy>{}
The dynamic parameters that are provided to the Enemy constructor are declared by providing extra generic arguments to the PlaceholderFactory<> base class of Enemy.Factory. PlaceholderFactory<> contains a Create method with the given parameter types, which can then be called by other classes such EnemySpawner
public void Tick(){...}
if (ShouldSpawnNewEnemy())
{
var newSpeed = Random.Range(MIN_ENEMY_SPEED, MAX_ENEMY_SPEED);
var enemy = _enemyFactory.Create(newSpeed);
// ...
}
Enemy.Factory is always intentionally left empty and simply derives from the built-in Zenject PlaceholderFactory<> class, which handles the work of using the DiContainer to construct a new instance of Enemy. It is called PlaceholderFactory because it doesn't actually control how the object is created directly.
There is no requirement that the Enemy.Factory class be a nested class within Enemy
You could install it like this instead, and bypass the need for a nested factory class:
Container.BindFactory<Enemy, PlaceholderFactory<Enemy>>()
However, this comes with several drawbacks:
Changing the parameter list is not detected at compile time. If the PlaceholderFactory<Enemy> is injected directly all over the code base, when we add a speed parameter and therefore change it to PlaceholderFactory<float, Enemy>, then we will get runtime errors when zenject fails to find the PlaceholderFactory<Enemy> dependency (or validation errors if you use validation). However, if we use a derived Enemy.Factory class, then if we later decide to derive from PlaceholderFactory<float, Enemy> instead, we will get compiler errors instead (at every place that calls the Create method) which is easier to catch
It's less verbose. Injecting Enemy.Factory everywhere is much more readable than PlaceholderFactory<float, Enemy>, especially as the parameter list grows.
The way that the object is created is declared in an installer in the same way it is declared for non-factory dependencies
For example, if our Enemy class was a MonoBehaviour on a prefab, we could install it like this instead
public class Enemy : MonoBehaviour{
Player _player;
// Note that we can't use a constructor anymore since we are a MonoBehaviour now
[Inject]
public void Construct(Player player) {
_player = player;
}
public class Factory : PlaceholderFactory<Enemy>{
}
}
public class TestInstaller : MonoInstaller{
public GameObject EnemyPrefab;
public override void InstallBindings(){
Container.BindInterfacesTo<EnemySpawner>().AsSingle();
Container.Bind<Player>().AsSingle();
Container.BindFactory<Enemy, Enemy.Factory ().FromComponentInNewPrefab(EnemyPrefab);
}
}
Binding Syntax
Container.BindFactory<ContractType, PlaceholderFactoryType>()
.WithId(Identifier)
.WithFactoryArguments(Factory Arguments)
.To<ResultType>()
.FromConstructionMethod()
.AsScope()
.WithArguments(Arguments)
.OnInstantiated(InstantiatedCallback)
.When(Condition)
.NonLazy()
.(Copy|Move)Into(All|Direct)SubContainers();
ContractType
The contract type returned from the factory Create method
// Is this the same as Interface?
PlaceholderFactoryType
The class deriving from PlaceholderFactory<>
WithFactoryArguments
If you want to inject extra arguments into your placeholder factory derived class, you can include them here
Note that WithArguments applies to the actual instantiated type and not the factory
Scope
Note that unlike for non-factory bindings, the default is AsCached instead of AsTransient, which is almost always what you want for factories, so in most cases you can leave this unspecified