From Zenject

Use these tables to translate a Zenject or Extenject project. The binding vocabulary carries over: Bind<TContract>().To<TConcrete>().AsSingle(), [Inject] on members, MonoInstaller, ProjectContext, SceneContext and GameObjectContext all exist in Onity with the same roles. Five behaviors differ and are worth reading before the tables: Onity is last-binding-wins instead of throwing on a duplicate bind; it selects the public constructor with the most parameters (or the single [Inject] constructor) instead of the fewest; it detects circular dependencies at resolve time; its AsScoped() is a lifetime, not a sub-container; and signals are replaced by the messaging pillar. Everything in these tables is verified against the Onity 0.8.2 source; an API that is not listed does not exist.

Contents:

Binding

Zenject Onity Notes
Container.Bind<IFoo>().To<Foo>().AsSingle() container.Bind<IFoo>().To<Foo>().AsSingle() Identical. One instance in the declaring scope.
Container.Bind<IFoo>().To<Foo>().AsTransient() container.Bind<IFoo>().To<Foo>().AsTransient() A new instance per resolve.
Container.Bind<IFoo>().To<Foo>().AsCached() container.Bind<IFoo>().To<Foo>().AsSingle() There is no separate cached lifetime.
Container.Bind<Foo>().AsSingle() (self-bind) container.Bind<Foo>().AsSingle() To defaults to the contract type.
Container.Bind<Foo>().ToSelf().FromNew().AsSingle() container.Bind<Foo>().AsSingle() The ToSelf and FromNew ceremony has no counterpart.
Container.Bind<IFoo>().To<Foo>().AsSingle().NonLazy() container.Bind<IFoo>().To<Foo>().AsSingle().NonLazy() Created at Build(). NonLazy() before the lifetime throws OnityBindingException.
Container.BindInstance(foo) container.BindInstance<IFoo>(foo) Caller-owned. A null instance throws OnityBindingException.
Container.BindInterfacesTo<Foo>().AsSingle() container.BindInterfacesTo<Foo>().AsSingle() One instance under every interface; the concrete type is not bound. Throws when Foo has no interfaces.
Container.BindInterfacesAndSelfTo<Foo>().AsSingle() container.BindInterfacesAndSelfTo<Foo>().AsSingle() One instance under the concrete type and every interface.
Container.Bind(typeof(IA), typeof(IB)).To<C>().AsSingle() container.BindInterfacesAndSelfTo<C>().AsSingle() or BindInterfacesTo<C>() No multi-type Bind(typeof, typeof) overload. Two separate binds would create two instances.
Container.Bind<IFoo>().To<Foo>().WithId("x").AsSingle() container.Bind<IFoo>().To<Foo>().WithId("x").AsSingle() Ids compare by value. Call WithId before the lifetime.
Container.Bind<IFoo>().To<Foo>().WhenInjectedInto<Bar>() container.Bind<IFoo>().To<Foo>().WhenInjectedInto<Bar>().AsSingle() The consumer type and its derived types. Two matching conditions throw OnityResolveException; WhenNotInjectedInto has no counterpart.
Container.Bind<IFoo>().FromSubContainerResolve().ByInstaller<FooInstaller>() container.Bind<IFoo>().FromSubContainerResolve(scope => { ... }) The installer is a delegate that binds IFoo inside the child; see Sub-container exports.
Container.Unbind<IFoo>() container.Unbind<IFoo>(), Unbind<IFoo>(id) Removes the local bindings, also after Build().
Container.Rebind<IFoo>().To<Foo2>().AsSingle() container.Rebind<IFoo>().To<Foo2>().AsSingle() Replaces the local binding when the lifetime is selected.
Duplicate Bind<Foo>().AsSingle() throws container.Bind<Foo>().AsSingle() twice: the last binding wins Divergence. Nothing throws.

Zenject’s scoping through sub-containers maps to a lifetime: AsScoped() creates one instance per resolving scope, so a GameObjectContext or child container that resolves a parent’s scoped binding gets its own instance. See Lifetimes.

Injection

Onity.DI.InjectAttribute targets constructors, fields, properties, methods and parameters, and has one property, Id.

Zenject Onity Notes
Constructor injection Constructor injection Identical and preferred.
[Inject] public Foo(IBar bar) [Inject] public Foo(IBar bar) A single [Inject] constructor wins over the others.
Several constructors: the one with the fewest arguments Several constructors: the public one with the most parameters Divergence. A non-public constructor is used only when no public one exists.
[Inject] IClock m_clock; [Inject] private IClock m_clock; Field injection, private allowed. Static members are never injected.
[Inject] public ILogger Logger { get; set; } [Inject] public ILogger Logger { get; set; } A setter is required; a get-only or indexer property throws OnityBindingException.
[Inject] void Init(IConfig config) [Inject] private void Init(IConfig config) Runs after the constructor, fields and properties. Cannot be generic.
[Inject(Id = "x")] IFoo m_foo; [Inject(Id = "x")] private IFoo m_foo; Also on properties and on individual constructor or method parameters.
[Inject(Optional = true)] none Bind the dependency, or use TryResolve in glue code.
Container.Inject(existing) container.Inject(existing) Member-injects an existing object. A null target throws OnityResolveException.
Container.InjectGameObject(root) context.InjectGameObject(root) On the owning OnityContext; it skips objects owned by a nearer context.

Member injection order is base class, then derived class; within a type fields, then properties, then methods.

Resolution

Zenject Onity Notes
Container.Resolve<IFoo>() container.Resolve<IFoo>() Throws OnityResolveException when unresolvable.
Container.Resolve(typeof(IFoo)) container.Resolve(typeof(IFoo)) Runtime-type form.
Container.ResolveId<IFoo>("x") container.Resolve<IFoo>("x") Identified resolve; Resolve(Type, id) for the runtime-type form.
Container.TryResolve<IFoo>() (null on miss) container.TryResolve<IFoo>(out IFoo foo) Returns bool; the identified form is TryResolve<IFoo>("x", out foo).
Container.HasBinding<IFoo>() container.CanResolve(typeof(IFoo)) No construction.
Container.ResolveAll<IFoo>() container.Resolve<IReadOnlyList<IFoo>>() Or IEnumerable<IFoo>, IReadOnlyCollection<IFoo>, IList<IFoo>, ICollection<IFoo>, List<IFoo>, IFoo[]. There is no ResolveAll method.
DiContainer injected into a class IResolver or OnityContainer Both resolve to the active scope; inject IResolver in a factory.

Factories

Onity has no fluent factory body and no generated placeholder factory. You author an IFactory<...> (Onity.Factory) and bind it with BindFactory; the factory is bound with BindInterfacesAndSelfTo<TFactory>().AsSingle().

Zenject Onity Notes
Container.BindFactory<Foo, Foo.Factory>() container.BindFactory<Foo, FooFactory>() where FooFactory : IFactory<Foo> Resolve IFactory<Foo> and call Create().
Container.BindFactory<string, Foo, Foo.Factory>() container.BindFactory<string, Foo, FooFactory>() where FooFactory : IFactory<string, Foo> Resolve IFactory<string, Foo> and call Create(id).
Two-argument factory container.BindFactory<string, int, Foo, FooFactory>() where FooFactory : IFactory<string, int, Foo> Two parameters is the largest arity; wrap more in a struct.
PlaceholderFactory<T> subclass a hand-written IFactory<...> class The factory takes services in its constructor and builds the value itself.
BindMemoryPool<Foo, Foo.Pool>(), MonoMemoryPool BindPooledFactory(prefab, parent, defaultCapacity, maxSize); OnityObjectPool<T> for plain objects Binds IPool<T> and IFactory<T>. Reset logic goes in IPoolHooks. See Factories and Pooling.
OnSpawned and OnDespawned IPoolHooks.OnPoolGet() and OnPoolRelease() On the pooled component.
Container.Instantiate<Foo>(args) none Author an IFactory<TParam, TValue>.
using Onity.Factory;            // IFactory<TParam, TValue>

public sealed class EnemyFactory : IFactory<EnemySpawnRequest, Enemy>
{
    private readonly IClock m_clock;

    public EnemyFactory(IClock clock)   // services come through the constructor
    {
        m_clock = clock;
    }

    public Enemy Create(EnemySpawnRequest request)
    {
        return new Enemy(request.Id, request.Level, m_clock);
    }
}

Bind and use (fragment): container.BindFactory<EnemySpawnRequest, Enemy, EnemyFactory>(); then container.Resolve<IFactory<EnemySpawnRequest, Enemy>>().Create(request).

Automatic lifecycle

In Zenject you bind the type and rely on BindInterfacesAndSelfTo to expose the lifecycle interfaces. In Onity, binding a singleton, a scoped binding or a bound instance that implements a lifecycle interface (Onity.DI) is enough; the container collects it at Build(), and the owning context pumps the ticks. Transients are not collected.

Zenject Onity Notes
IInitializable.Initialize() IOnityInitializable.Initialize() Once, at the end of Build(), in binding-registration order.
ITickable.Tick() IOnityTickable.Tick() From the context’s Update.
IFixedTickable.FixedTick() IOnityFixedTickable.FixedTick() From FixedUpdate.
ILateTickable.LateTick() IOnityLateTickable.LateTick() From LateUpdate.
async startup through a UniTask method IOnityAsyncInitializable.InitializeAsync(CancellationToken) Awaited in BuildAsync, which a context runs from Start. See Async initialization.
IDisposable on a bound singleton IDisposable on a bound singleton Disposed with the scope, after the scope token is canceled.
IGuiRenderable, IPoolable none Draw from a MonoBehaviour; use IPoolHooks for pooled components.
using Onity.DI;                 // IOnityInitializable, IOnityTickable

public sealed class WaveDirector : IOnityInitializable, IOnityTickable
{
    public void Initialize()
    {
        // Runs once at Build(), in registration order.
    }

    public void Tick()
    {
        // Runs every Update from the owning context.
    }
}

Binding it is the whole wiring (fragment): container.BindInterfacesAndSelfTo<WaveDirector>().AsSingle();.

Collection injection

Bind several implementations of one contract, then inject the set. The set is gathered from every explicit binding in this scope and its ancestors, ancestors first. Fragment:

container.Bind<IDamageRule>().To<CritRule>().AsSingle();
container.Bind<IDamageRule>().To<ArmorRule>().AsSingle();
using System.Collections.Generic;

public sealed class DamagePipeline
{
    private readonly IReadOnlyList<IDamageRule> m_rules;

    public DamagePipeline(IReadOnlyList<IDamageRule> rules)   // Zenject: List<IDamageRule>
    {
        m_rules = rules;
    }
}

A plain Resolve<IDamageRule>() returns the last binding, so single-resolve call sites keep working; only a collection-typed request gathers all of them.

Open generic bindings

Bind an open generic definition once; any closed form resolves on demand and is cached as a normal binding (fragment):

container.Bind(typeof(IRepository<>)).To(typeof(Repository<>)).AsSingle();
IRepository<PlayerProfile> profiles = container.Resolve<IRepository<PlayerProfile>>();

NonLazy() is not supported on an open generic binding. On IL2CPP the closed type must survive stripping.

Scopes and contexts

Zenject Onity Notes
ProjectContext prefab in Resources ProjectContext prefab at Assets/Resources/Onity/ProjectContext.prefab Create it with Onity/Contexts/Create ProjectContext Prefab; loaded before the first scene.
SceneContext SceneContext (Onity.Unity.Contexts) Child of the ProjectContext when one exists.
GameObjectContext GameObjectContext Child of the nearest context above it, else the ProjectContext.
MonoInstaller.InstallBindings() using Container MonoInstaller.InstallBindings(OnityContainer container) (Onity.Unity.Installers) The container is a parameter.
ScriptableObjectInstaller BindScriptableObject(asset) inside a MonoInstaller Member-injects the asset and binds it as an instance.
Container.CreateSubContainer() new OnityContainer(parent) A child inherits the parent’s bindings; a child binding shadows the parent only inside the child.
Sub-container facade FromSubContainerResolve(install) Exports one contract from a child installed per requesting scope.
Container.Dispose() or scene unload container.Dispose(); a context disposes in OnDestroy Cancels the scope token, then disposes child scopes, scoped instances and singletons in reverse registration order.
none container.Build(), await container.BuildAsync(token) A context calls both. Build callbacks cannot be registered after the build.

Every context binds the container, IResolver, IOnityScopeLifetime, the context, MessageBroker and OnityEventHub, so these need no installer line.

Signals and messaging

Zenject signals map to the messaging pillar. There is no signal installer and no declaration step: the broker is bound by every context, and any struct or class is a message.

Zenject Onity Notes
SignalBusInstaller.Install(Container) none Every context binds MessageBroker and OnityEventHub.
Container.DeclareSignal<T>() none, or container.BindMessageChannel<T>() Bind the channel only when a service injects IPublisher<T> or ISubscriber<T> alone.
SignalBus.Fire(signal) OnityEventHub.Publish(message) in services; OnityEvent.Publish(this, message) in MonoBehaviours OnityEventHub is in Onity.Unity.Messaging, OnityEvent in Onity.Unity.
SignalBus.Subscribe<T>(handler) and Unsubscribe OnityEventHub.Subscribe<T>(handler).AddTo(scope); OnityEvent.Subscribe<T>(this, handler) Subscriptions are IDisposable; retain them with AddTo(scope), AddTo(this) or TakeUntilDisable(this). The owner overload of OnityEvent.Subscribe disposes on destroy.
SignalBus.GetStream<T>() OnityEventHub.Observe<T>(); OnityEvent.Observe<T>(this) An IOnityObservable<T>; apply Where, Select and the other operators.
Container.BindSignal<T>().ToMethod(...) a subscription in the handler’s constructor, retained with AddTo(scope) See Events and Messaging.

Errors

Onity has two DI exception types in Onity.DI. OnityResolveException covers resolve and inject failures: unbound contracts, circular dependencies, failed construction, ambiguous conditional bindings, null targets and use after dispose. OnityBindingException covers configuration failures: null or non-assignable implementations, BindInterfacesTo on a type without interfaces, multiple [Inject] constructors, setterless, indexer or generic [Inject] members, callbacks registered after build, and NonLazy before a lifetime. Map a ZenjectException catch onto these two.

Not supported

These Zenject features have no Onity counterpart. Use the replacement.

Zenject feature Do this in Onity
Container.Instantiate<T>(args) Author an IFactory<TParam, TValue> and BindFactory.
Fluent factory bodies (FromMethod, FromFactory, FromIFactory) and generated PlaceholderFactory Write the factory class.
[Inject(Optional = true)] Bind the dependency, or TryResolve in glue code.
BindInstance(null) Pass a non-null instance.
Signals (SignalBus, DeclareSignal, BindSignal) Messaging: OnityEventHub, OnityEvent, BindMessageChannel<T>.
WhenNotInjectedInto and other condition forms WhenInjectedInto<TConsumer>() only; otherwise distinct contract types or a factory.
Multi-type Bind(typeof(IA), typeof(IB)) BindInterfacesAndSelfTo or BindInterfacesTo.
AsCached as a distinct lifetime AsSingle().
ResolveAll Request a collection type.
Duplicate-binding errors Last binding wins; use Rebind when the replacement should be explicit.

Features that ship and need no workaround: AsScoped(), WithId, [Inject(Id = ...)], WhenInjectedInto, Unbind, Rebind, FromSubContainerResolve, collection injection, open generic bindings, the automatic lifecycle, and pools (OnityObjectPool<T>, PrefabComponentPool<T>, BindPooledFactory).

Lifetime and async

Zenject has no scope token and no awaitable startup of its own. In Onity every scope has a LifetimeToken that is canceled before the scope disposes its services; services inject IOnityScopeLifetime for it, MonoBehaviours call GetScopeCancellationToken(), and IOnityAsyncInitializable with OnityContext.WaitReadyAsync replaces hand-written startup ordering. Read Lifecycle and Scopes before you move startup code, and From UniTask if the project also uses UniTask.