Scope
A scopeScopeMonoBehaviour that declares bindings for a part of your hierarchy. is a MonoBehaviour that declares dependency bindingsBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. for a part of your hierarchy.
In Saneject, you create a scopeScopeMonoBehaviour that declares bindings for a part of your hierarchy. by inheriting from Scope and implementing DeclareBindings(). Every bindingBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. you
declare in that method tells Saneject how to resolve a dependency type.
At injection time, Saneject uses scopeScopeMonoBehaviour that declares bindings for a part of your hierarchy. bindingsBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. to resolve [Inject] fields, properties and methods for components
below that scopeScopeMonoBehaviour that declares bindings for a part of your hierarchy.'s Transform. At runtime, the same scopeScopeMonoBehaviour that declares bindings for a part of your hierarchy. also performs early setup for global registrationsGlobal registrationEntry added to GlobalScope at runtime, keyed by the component's concrete type and owned by the caller that registered it. and
runtime proxyRuntime proxyScriptableObject placeholder asset (RuntimeProxy<TComponent>) injected into interface members at editor time and swapped to the real instance during scope startup. swapping.
ScopesScopeMonoBehaviour that declares bindings for a part of your hierarchy. work on scene objectsScene objectNon-prefab GameObject in a scene., prefab instancesPrefab instanceInstantiated prefab placed in a scene or nested inside another prefab., and prefab assetsPrefab assetReusable prefab definition in the Project window.. How they participate in injection and how their
bindingsBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. resolve across scenes and prefabs depends on context filteringContext filteringInjection-run prefilter that decides which transforms and injection targets are active for a run. and context isolationContext isolationProject setting (UseContextIsolation) that controls whether dependency resolution can cross context boundaries. settings.
See Context for details.
Declaring bindings in a scope
BindingsBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. are declared directly inside DeclareBindings():
using Plugins.Saneject.Runtime.Scopes;
public class EnemyScope : Scope
{
protected override void DeclareBindings()
{
BindComponent<AIController>()
.FromScopeSelf();
}
}
This declares a bindingBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. for AIController and tells Saneject where to look for the instance (FromScopeSelf() means
the scopeScopeMonoBehaviour that declares bindings for a part of your hierarchy.'s own Transform).
For more details and bindingBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. examples, see Bindings.
Hierarchy, overrides, and fallback
Saneject tries to resolve each injection targetInjection targetComponent with injected fields, properties or methods. (Component with injected fields, properties, methods) from the nearest
scopeScopeMonoBehaviour that declares bindings for a part of your hierarchy. at the same transform or above it first, then walks up parent scopesScopeMonoBehaviour that declares bindings for a part of your hierarchy. until a matching bindingBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. is found. That means
child scopesScopeMonoBehaviour that declares bindings for a part of your hierarchy. naturally override parent scopesScopeMonoBehaviour that declares bindings for a part of your hierarchy. for the same requested type.
Example:
flowchart TD
subgraph Editor
DI["Scene injection pass"]
end
subgraph Scene
DI -->|Inject| EnemyAudioService["Enemy.audioService (IAudioService)"]
DI -->|Inject| EnemyAIController["Enemy.aiController (AIController)"]
EnemyAudioService -->|"Try resolve IAudioService -> Binding not found"| EnemyScope
EnemyAIController -->|"Try resolve AIController -> Binding found"| EnemyScope
EnemyScope -->|"Try resolve IAudioService -> Binding found"| RootScope
end
using Plugins.Saneject.Runtime.Scopes;
public class RootScope : Scope
{
protected override void DeclareBindings()
{
// AudioServiceAsset is a Unity asset type that implements IAudioService.
BindAsset<IAudioService, AudioServiceAsset>()
.FromResources("Audio/Service");
}
}
using Plugins.Saneject.Runtime.Scopes;
public class EnemyScope : Scope
{
protected override void DeclareBindings()
{
// Enemy-local AIController only. No IAudioService binding here.
BindComponent<AIController>()
.FromScopeSelf();
}
}
using Plugins.Saneject.Runtime.Attributes;
using UnityEngine;
public partial class Enemy : MonoBehaviour
{
[Inject, SerializeInterface]
private IAudioService audioService; // Resolved from RootScope (fallback)
[Inject]
private AIController aiController; // Resolved from EnemyScope
}
IAudioService is not bound in EnemyScope, so Saneject walks up to RootScope and resolves it there. AIController
is bound in EnemyScope, so it resolves locally without fallback.
Runtime behavior
Almost everything in Saneject happens at edit-time. However, a few things need to happen at runtime to facilitate dependencies between contextsContextSerialization boundary Saneject uses during injection to decide scope traversal and candidate eligibility. that Unity cannot serialize (scene ↔ other scene, scene ↔ prefab assetPrefab assetReusable prefab definition in the Project window.).
Global components in scopes
ScopesScopeMonoBehaviour that declares bindings for a part of your hierarchy. can declare globally/statically available components with BindGlobal<T>():
using Plugins.Saneject.Runtime.Scopes;
public class BootstrapScope : Scope
{
protected override void DeclareBindings()
{
BindGlobal<AudioManager>()
.FromScopeSelf();
}
}
How it works:
- During editor injection, Saneject resolves the global component and serializes it in the same
Scopethat declares the global bindingBindingInstruction declared in aScopethat tells Saneject what to resolve, how to inject it, and where to search.. - At runtime,
Scope.Awake()registers those serialized components intoGlobalScope(a static service locatorService locatorObject or API that lets runtime code request dependencies on demand instead of receiving them through injection. In Saneject docs, this usually refers toGlobalScope.). - This is usually used as a cheap lookup mechanism for runtime proxiesRuntime proxy
ScriptableObjectplaceholder asset (RuntimeProxy<TComponent>) injected into interface members at editor time and swapped to the real instance during scope startup. that resolve withFromGlobalScope(). - In
Scope.OnDestroy(), the scopeScopeMonoBehaviourthat declares bindings for a part of your hierarchy. unregisters what it registered fromGlobalScope, meaning that global components per localScopehave the same lifetime as the scopeScopeMonoBehaviourthat declares bindings for a part of your hierarchy.. Scopehas default execution order-10000, so scopeScopeMonoBehaviourthat declares bindings for a part of your hierarchy. runtime operations run before normal componentAwaketo avoid startup race conditions/null access issues.
Only one global registrationGlobal registrationEntry added to GlobalScope at runtime, keyed by the component's concrete type and owned by the caller that registered it. per concrete component type is allowed. Duplicate global bindingsBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. for the same type are
reported as invalid.
See Global scope for details.
Runtime proxy swap targets
A RuntimeProxy is an auto-generated ScriptableObject used as an editor-time stand-in for a real interface dependency
when that real reference cannot be serialized directly in the current Scope contextContextSerialization boundary Saneject uses during injection to decide scope traversal and candidate eligibility. (for example, prefab to scene
references).
During editor injection, when Saneject injects a runtime proxyRuntime proxyScriptableObject placeholder asset (RuntimeProxy<TComponent>) injected into interface members at editor time and swapped to the real instance during scope startup. into an interface field, it registers the owning
component as a proxy swap targetProxy swap targetComponent that implements IRuntimeProxySwapTarget and is asked at runtime startup to replace proxy references with resolved real instances. in the nearest Scope.
At runtime, Scope.Awake() (execution order -10000) runs right after global registrationGlobal registrationEntry added to GlobalScope at runtime, keyed by the component's concrete type and owned by the caller that registered it. and calls Roslyn-generated
SwapProxiesWithRealInstances() on each registered swap target. That generated method replaces proxy references with
the real instances using normal field assignment, with no reflection.
The full proxy mechanism is documented in Runtime proxies.
Proxy swap flow:
- A bindingBindingInstruction declared in a
Scopethat tells Saneject what to resolve, how to inject it, and where to search. usesFromRuntimeProxy()for an interface dependency. - Injection writes a runtime proxyRuntime proxy
ScriptableObjectplaceholder asset (RuntimeProxy<TComponent>) injected into interface members at editor time and swapped to the real instance during scope startup. into the interface field and registers the owner as a swap target in the nearestScope. - In
Scope.Awake(), the scopeScopeMonoBehaviourthat declares bindings for a part of your hierarchy. callsSwapProxiesWithRealInstances(), and the field is reassigned to the real instance.
Typical proxy bindingBindingInstruction declared in a Scope that tells Saneject what to resolve, how to inject it, and where to search. setup:
public class CombatScope : Scope
{
protected override void DeclareBindings()
{
BindComponent<ICombatService, CombatService>()
.FromRuntimeProxy()
.FromGlobalScope();
BindGlobal<CombatService>()
.FromScopeSelf();
}
}
Typical consumer:
using Plugins.Saneject.Runtime.Attributes;
using UnityEngine;
public partial class CombatHud : MonoBehaviour
{
[Inject, SerializeInterface]
private ICombatService combatService;
}
See Runtime proxies and Serialized interfaces for details.