Zenject

m

Unity3D

Editor

Scene

GameObject

SceneContext.cs

Installers

Scriptable

.asset

Mono

.cs

Prefabs

.gameObject

Edit -> Zenject -> Validate Current Scene

Library

Injection

Constructor

c1

Field

c1

Property

c1

Method

c1

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

Click here to center your diagram.
Click here to center your diagram.