From VContainer
Use these tables to translate a VContainer project. VContainer registers with builder.Register<TImpl>(Lifetime.Singleton).As<TInterface>() inside a LifetimeScope; Onity binds with container.Bind<TInterface>().To<TImpl>().AsSingle() inside a MonoInstaller that a context runs. The three lifetimes map one to one (Lifetime.Scoped is AsScoped()), entry points become lifecycle interfaces that the container collects without a registration call, and IAsyncStartable becomes IOnityAsyncInitializable. The behaviors that differ: BindInstance rejects null, circular dependencies surface at resolve time rather than at build, and there is no Func<> factory registration. Everything here is verified against the Onity 0.8.2 source; an API that is not listed does not exist.
Contents:
- Registration
- Injection
- Resolution
- Factories
- Collection injection
- Entry point lifecycle
- Open generic registration
- Scopes and lifecycle
- Errors
- Not supported
- Lifetime and async
Registration
| VContainer | Onity | Notes |
|---|---|---|
builder.Register<Impl>(Lifetime.Singleton).As<IService>() | container.Bind<IService>().To<Impl>().AsSingle() | One instance in the declaring scope, shared with children. |
builder.Register<Impl>(Lifetime.Scoped).As<IService>() | container.Bind<IService>().To<Impl>().AsScoped() | One instance per resolving scope, built with that scope’s dependencies and disposed with it. |
builder.Register<Impl>(Lifetime.Transient).As<IService>() | container.Bind<IService>().To<Impl>().AsTransient() | A new instance per resolve. |
builder.Register<Impl>(Lifetime.Singleton) (self) | container.Bind<Impl>().AsSingle() | To defaults to the contract. |
builder.Register<Impl>(Lifetime.Singleton).As<IA>().As<IB>().AsSelf() | container.BindInterfacesAndSelfTo<Impl>().AsSingle() | One instance under the concrete type and every interface. |
builder.Register<Impl>(Lifetime.Singleton).AsImplementedInterfaces() | container.BindInterfacesTo<Impl>().AsSingle() | Interfaces only. Throws OnityBindingException when Impl has no interfaces. |
builder.RegisterInstance(instance).As<IService>() | container.BindInstance<IService>(instance) | Caller-owned. A null instance throws OnityBindingException. |
builder.RegisterInstance(instance).AsImplementedInterfaces().AsSelf() | container.BindInstance<IA>(instance); container.BindInstance<Impl>(instance); | One BindInstance per contract; the object is the same. |
builder.Register<Impl>(Lifetime.Singleton).As<IService>().Keyed("x") | container.Bind<IService>().To<Impl>().WithId("x").AsSingle() | Consume with [Inject(Id = "x")] or Resolve<IService>("x"). |
builder.Register<IService, Impl>(Lifetime.Singleton) | container.Bind<IService>().To<Impl>().AsSingle() | Same contract-first shape. |
builder.RegisterBuildCallback(resolver => ...) | container.RegisterBuildCallback(resolver => ...) | Also RegisterBuildCallbackAsync(Func<IResolver, Task>) and the token form. |
builder.RegisterComponentInHierarchy<T>(), RegisterComponentInNewPrefab<T>(), RegisterComponentOnNewGameObject<T>() | none | Bind the component instance after you obtain it, or use BindScriptableObject and BindPooledFactory (Onity.Unity.Installers). |
builder.RegisterEntryPoint<T>() | container.BindInterfacesAndSelfTo<T>().AsSingle() where T implements a lifecycle interface | Binding is the registration; see Entry point lifecycle. |
| Registering the same contract twice | the last binding wins | No error in either library. |
Two separate Bind<IA>().To<C>() and Bind<IB>().To<C>() calls create two C instances. VContainer’s As<IA>().As<IB>() sharing is BindInterfacesAndSelfTo<C>() or BindInterfacesTo<C>().
Injection
Onity.DI.InjectAttribute targets constructors, fields, properties, methods and parameters.
| VContainer | Onity | Notes |
|---|---|---|
| Constructor injection | Constructor injection | Identical and preferred. |
| The greediest resolvable constructor | The public constructor with the most parameters | A non-public constructor is used only when no public one exists. |
[Inject] public Foo(IBar bar) | [Inject] public Foo(IBar bar) | The marked constructor wins. Two marked constructors throw OnityBindingException. |
[Inject] IBar m_bar; | [Inject] private IBar m_bar; | Field injection, private allowed. Static members are never injected. |
[Inject] public IBar Bar { get; set; } | [Inject] public IBar Bar { get; set; } | A setter is required. |
[Inject] public void Construct(IBar bar) | [Inject] private void Construct(IBar bar) | Runs after the constructor, fields and properties. Cannot be generic. |
[Key("x")] on a parameter | [Inject(Id = "x")] on the parameter, field or property | Selects a WithId binding. |
resolver.Inject(instance) | container.Inject(instance) | Member-injects an existing object. |
resolver.InjectGameObject(gameObject) | context.InjectGameObject(root) | On the owning OnityContext. |
Member injection order is base class, then derived class; within a type fields, then properties, then methods.
Resolution
| VContainer | Onity | Notes |
|---|---|---|
resolver.Resolve<IFoo>() | container.Resolve<IFoo>() | Throws OnityResolveException when unresolvable. |
resolver.Resolve(typeof(IFoo)) | container.Resolve(typeof(IFoo)) | Runtime-type form. |
resolver.Resolve<IFoo>("x") (keyed) | container.Resolve<IFoo>("x") | Identified resolve; TryResolve<IFoo>("x", out foo) for the safe form. |
resolver.TryResolve<IFoo>(out var foo) | container.TryResolve<IFoo>(out IFoo foo) | Returns bool; out is null on a miss. |
inject IObjectResolver | inject IResolver or OnityContainer | Both resolve to the active scope. |
resolver.Resolve<IEnumerable<IFoo>>() | container.Resolve<IEnumerable<IFoo>>() | Or any of the other six collection shapes; see below. |
Unregistered type throws VContainerException | Unbound interface or abstract type throws OnityResolveException | An unbound concrete class resolves as an implicit transient. |
Factories
VContainer registers Func<...> factories with RegisterFactory. Onity has no Func<> factory and no Instantiate(args): author an IFactory<...> (Onity.Factory) and bind it with BindFactory, which binds it with BindInterfacesAndSelfTo<TFactory>().AsSingle().
| VContainer | Onity | Notes |
|---|---|---|
builder.RegisterFactory<TValue>(() => ...) | container.BindFactory<TValue, TFactory>() where TFactory : IFactory<TValue> | Resolve IFactory<TValue> and call Create(). |
builder.RegisterFactory<TParam, TValue>(param => ...) | container.BindFactory<TParam, TValue, TFactory>() where TFactory : IFactory<TParam, TValue> | Resolve IFactory<TParam, TValue> and call Create(param). |
builder.RegisterFactory<T1, T2, TValue>(...) | container.BindFactory<T1, T2, TValue, TFactory>() where TFactory : IFactory<T1, T2, TValue> | Two parameters is the largest arity. |
RegisterFactory with a resolver-bound delegate | the factory’s constructor takes the services | The container constructs the factory. |
WithParameter(...) on a registration | an IFactory<TParam, TValue> | Per-call values do not go through constructor injection. |
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). Prefab pools are BindPooledFactory(prefab, parent, defaultCapacity, maxSize); see Factories and Pooling.
Collection injection
VContainer collects every registration of a contract when you inject IEnumerable<T> or IReadOnlyList<T>. Onity does the same for IEnumerable<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IList<T>, ICollection<T>, List<T> and T[], gathered from 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) // VContainer: IEnumerable<IDamageRule>
{
m_rules = rules;
}
}
A plain Resolve<IDamageRule>() returns the last binding; only a collection-typed request gathers all of them.
Entry point lifecycle
VContainer runs entry points through RegisterEntryPoint. Onity collects a singleton, a scoped binding or a bound instance that implements a lifecycle interface (Onity.DI) at Build(); the owning context pumps the ticks. Transients are not collected.
| VContainer | Onity | Notes |
|---|---|---|
IInitializable.Initialize(), IStartable.Start() | IOnityInitializable.Initialize() | Once, at the end of Build(), in binding-registration order. |
IAsyncStartable.StartAsync(CancellationToken) | IOnityAsyncInitializable.InitializeAsync(CancellationToken) | A ValueTask. Awaited in BuildAsync, one initializer at a time; the token is the scope’s LifetimeToken. |
ITickable.Tick() | IOnityTickable.Tick() | From the context’s Update. |
IFixedTickable.FixedTick() | IOnityFixedTickable.FixedTick() | From FixedUpdate. |
ILateTickable.LateTick() | IOnityLateTickable.LateTick() | From LateUpdate. |
IPostInitializable, IPostStartable, IPostTickable, IPostLateTickable | none | Order within a phase follows binding-registration order; bind the later service later. |
builder.RegisterEntryPointExceptionHandler(...) | none | A faulted InitializeAsync ends BuildAsync; the context logs it and ReadyTask faults. |
using Onity.DI; // IOnityInitializable, IOnityTickable
public sealed class WaveDirector : IOnityInitializable, IOnityTickable
{
public void Initialize()
{
// Runs once at Build().
}
public void Tick()
{
// Runs every Update from the owning context.
}
}
Binding it is the whole wiring (fragment): container.BindInterfacesAndSelfTo<WaveDirector>().AsSingle();.
Open generic registration
Fragment:
container.Bind(typeof(IRepository<>)).To(typeof(Repository<>)).AsSingle();
IRepository<PlayerProfile> profiles = container.Resolve<IRepository<PlayerProfile>>();
The first resolve of a closed contract builds and caches it as a normal binding. NonLazy() is not supported on an open generic binding. On IL2CPP the closed type must survive stripping.
Scopes and lifecycle
| VContainer | Onity | Notes |
|---|---|---|
LifetimeScope (root, DontDestroyOnLoad) | ProjectContext (Onity.Unity.Contexts) | Prefab at Assets/Resources/Onity/ProjectContext.prefab, created with Onity/Contexts/Create ProjectContext Prefab, loaded before the first scene. |
LifetimeScope in a scene | SceneContext | Child of the ProjectContext when one exists. |
LifetimeScope on a prefab, EnqueueParent, CreateChild | GameObjectContext, or new OnityContainer(parent) in code | The child inherits the parent’s bindings; a child binding shadows the parent only inside the child. |
Configure(IContainerBuilder builder) | MonoInstaller.InstallBindings(OnityContainer container) (Onity.Unity.Installers) | Assigned to the context’s Installers list. |
autoRun, autoInjectGameObjects | Auto Inject Hierarchy and Run Async Build Callbacks on the context | Both default on. |
Lifetime.Scoped | AsScoped() | Shared within a resolving scope, distinct across sibling scopes. |
IDisposable services disposed on scope dispose | container.Dispose(); a context disposes in OnDestroy | Cancels the scope token, then disposes child scopes, scoped instances and singletons in reverse registration order. |
| Build-time validation of the whole graph | resolve-time validation | Build() runs NonLazy bindings and the callbacks; missing or circular bindings throw OnityResolveException at resolve time. |
Every context binds the container, IResolver, IOnityScopeLifetime, the context, MessageBroker and OnityEventHub, so these need no installer line. VContainer’s IObjectResolver injected into a class becomes IResolver.
Errors
VContainer throws VContainerException for registration and resolution problems. Onity splits them: OnityBindingException for configuration (null instance, non-assignable implementation, multiple [Inject] constructors, setterless, indexer or generic [Inject] members, callbacks registered after build, NonLazy before a lifetime) and OnityResolveException for resolution (unbound contract, circular dependency, failed construction, ambiguous conditional bindings, null target, use after dispose). Both live in Onity.DI.
Not supported
| VContainer feature | Do this in Onity |
|---|---|
Func<...> factory registration and RegisterFactory | Author an IFactory<...> and BindFactory. |
RegisterInstance(null) | Pass a non-null instance. |
RegisterComponentInHierarchy, RegisterComponentInNewPrefab, RegisterComponentOnNewGameObject | Bind the obtained component with BindInstance, or use BindScriptableObject and BindPooledFactory. |
WithParameter on a registration | An IFactory<TParam, TValue>. |
IPostInitializable and the other post-phase entry points | Binding-registration order within the phase. |
RegisterEntryPointExceptionHandler | Handle inside the service; a faulted InitializeAsync faults ReadyTask. |
| Build-time graph validation | Resolve-time exceptions; NonLazy() surfaces construction errors at Build(). |
Features that ship and need no workaround: AsScoped() for Lifetime.Scoped, keyed bindings (WithId, [Inject(Id = ...)]), conditional bindings (WhenInjectedInto), collection injection, open generic registration, the lifecycle interfaces including IOnityAsyncInitializable, Unbind and Rebind, and pools (OnityObjectPool<T>, PrefabComponentPool<T>, BindPooledFactory).
Lifetime and async
VContainer’s LifetimeScope disposes its container; Onity adds a scope token that is canceled before the services are disposed. Services inject IOnityScopeLifetime for it, MonoBehaviours call GetScopeCancellationToken(), and async startup moves from IAsyncStartable or a UniTask entry point to IOnityAsyncInitializable; code that must wait for the scope awaits OnityContext.WaitReadyAsync(). Read Lifecycle and Scopes before you move startup code, and From UniTask if the project also uses UniTask.