Field, property & method injection
Field, property, and method injection is how Saneject writes resolved dependencies into components during an editor injection run.
For binding setup details, see Binding and Scope.
How it works
At a high level, Saneject performs these steps:
- Scans active injection targets for
[Inject]fields and[Inject]methods. - Also scans nested
[Serializable]class instances inside those components. - For each injection site, finds a matching binding by walking from the nearest scope upward.
- Matches on requested type, collection shape (single vs array/list), and optional binding qualifiers (ID, target type, member name).
- Locates dependency objects from the selected binding and applies context isolation rules.
- Injects values in this order: global registrations, fields and auto-properties, then methods.
Important behavior:
- For single-value sites, Saneject assigns the first located candidate.
- For
T[]andList<T>, Saneject assigns all located candidates. - If no match is found, Saneject records missing binding or missing dependency errors and injects
null. suppressMissingErrorsonly suppresses missing binding and missing dependency logs. It does not suppress method invocation exceptions.
Inject attribute options
InjectAttribute supports these forms:
| Syntax | Meaning |
|---|---|
[Inject] |
Type-only matching. |
[Inject("id")] |
Type + ID matching. |
[Inject(suppressMissingErrors: true)] |
Type matching, but suppress missing binding/dependency logs. |
[Inject("id", suppressMissingErrors: true)] |
Type + ID matching, with suppressed missing binding/dependency logs. |
The ID value must match a binding qualifier declared with ToID(...).
Field injection
Annotate a field with [Inject]. Field injection is persisted through Unity serialization, so injected fields should be serializable.
In practice:
- Concrete fields should be
publicor[SerializeField] private. - Interface fields should use
[SerializeInterface], so Unity can serialize and show them in the inspector. Access modifier does not matter for this case. - Arrays and
List<>require collection bindings (BindComponents,BindAssets, or equivalent collection forms).
using System.Collections.Generic;
using Plugins.Saneject.Runtime.Attributes;
using UnityEngine;
using UnityEngine.UI;
public partial class HUDView : MonoBehaviour
{
// Concrete component dependency.
[Inject, SerializeField]
private Camera mainCamera;
// Same type can be disambiguated by ID.
[Inject("scoreText"), SerializeField]
private Text scoreText;
// Interface dependency.
[Inject, SerializeInterface]
private IScoreService scoreService;
// Interface collection dependency.
[Inject, SerializeInterface]
private List<IEnemyObservable> enemyObservables;
// Collection dependencies.
[Inject, SerializeField]
private AudioClip[] hitSfx;
[Inject, SerializeField]
private List<AudioClip> uiSfx;
// Optional dependency. Missing logs are suppressed.
[Inject(suppressMissingErrors: true), SerializeField]
private Text optionalDebugText;
}
using Plugins.Saneject.Runtime.Scopes;
public class HUDScope : Scope
{
protected override void DeclareBindings()
{
BindComponent<Camera>()
.ToMember("mainCamera")
.FromAnywhere();
BindComponent<Text>()
.ToID("scoreText")
.FromScopeDescendants(includeSelf: true);
BindComponent<IScoreService, ScoreService>()
.FromRuntimeProxy()
.FromGlobalScope();
BindComponents<IEnemyObservable>()
.FromAnywhere();
BindAssets<AudioClip>()
.ToMember("hitSfx")
.FromFolder("Assets/Game/Audio/Sfx/Hit");
BindAssets<AudioClip>()
.ToMember("uiSfx")
.FromFolder("Assets/Game/Audio/Sfx/UI");
}
}
Property injection
InjectAttribute targets fields and methods, but not normal properties.
For properties, use auto-properties with a field target:
[field: Inject][field: SerializeField]for concrete types[field: SerializeInterface]for interface types
using System.Collections.Generic;
using Plugins.Saneject.Runtime.Attributes;
using UnityEngine;
public partial class CombatHud : MonoBehaviour
{
[field: Inject, SerializeField]
public Camera MainCamera { get; private set; }
[field: Inject("playerRoot"), SerializeField]
public Transform PlayerRoot { get; private set; }
[field: Inject, SerializeInterface]
public ICombatService CombatService { get; private set; }
[field: Inject, SerializeInterface]
public IEnemyObservable[] EnemyObservables { get; private set; }
[field: Inject, SerializeField]
public List<AudioClip> UISounds { get; private set; }
}
Notes:
- This works for auto-properties because the compiler generates a backing field that Saneject can inject.
- Binding qualifier
ToMember("MainCamera")matches the logical property name, not the compiler-generated backing field name.
Method injection
Annotate a method with [Inject]. Saneject resolves each parameter from bindings and then invokes the method.
Because this invocation happens at editor-time, method injection is mostly useful for setup/configuration logic. Typical examples include configuring components whose source you cannot modify and add [Inject] to, applying values to built-in Unity components from injected config assets, validating setup, or running custom wiring logic inside your type.
Key rules:
- Qualifiers (
ID,ToTarget,ToMember) are matched against the method as a whole. - Method-level
IDandsuppressMissingErrorssettings apply to all parameters in that method. - Each parameter is resolved by its own requested type and shape.
- Methods are invoked after field and property injection.
- If any parameter is not resolved (
null), the method is not invoked. - Method exceptions are caught and logged. They do not break the overall injection run.
using System.Collections.Generic;
using Plugins.Saneject.Runtime.Attributes;
using UnityEngine;
using UnityEngine.UI;
public class CombatController : MonoBehaviour
{
[Inject("combatInit")]
private void Initialize(
ICombatService combatService,
Camera mainCamera,
IEnemyObservable[] enemyObservables,
List<EnemyController> enemies)
{
// Use injected parameters.
}
[Inject("optionalUI", suppressMissingErrors: true)]
private void TryWireOptionalUI(Text debugText, Button retryButton)
{
if (debugText == null || retryButton == null)
return;
}
}
Example bindings for Initialize:
protected override void DeclareBindings()
{
BindComponent<ICombatService, CombatService>()
.ToID("combatInit")
.ToMember("Initialize")
.FromScopeSelf();
BindComponent<Camera>()
.ToID("combatInit")
.ToMember("Initialize")
.FromAnywhere();
BindComponents<IEnemyObservable, EnemyManager>()
.ToID("combatInit")
.ToMember("Initialize")
.FromScopeDescendants(includeSelf: true);
BindComponents<EnemyController>()
.ToID("combatInit")
.ToMember("Initialize")
.FromScopeDescendants(includeSelf: true);
}
Supported dependency types
Saneject resolves dependencies from component bindings and asset bindings. That means injected objects are Unity objects, such as:
UnityEngine.ComponentinstancesUnityEngine.Objectassets, e.g.,ScriptableObject,AudioClip,Texture2D, etc.
Plain C# object (POCO) services are not supported.
For service-like patterns, use runtime proxies with component implementations. Runtime proxy bindings can resolve from global scope, loaded scenes, prefabs, or newly created GameObjects, with transient or singleton instance modes. See Runtime proxy for details.
Interface injection notes
Interface injection is supported for fields, auto-properties, and method parameters.
For serialized fields and auto-properties, use [SerializeInterface] so references are visible and persisted in Unity's inspector/serialization workflow. See Serialized interface.