Jeringa: Custom C# Attributes for Easy Dependency Injection
2026-01-19, Game Development
Dragging dependencies into the inspector can be a hassle, manually calling GetComponent() for each dependency can be too. A global access point like a Singleton could come with unexpected consecuences, and Dependency Injection (DI) systems like Zenject can be overkill for smaller projects.
For reasons like that I created a simple DI system to cover most of my basic necessities when gathering dependencies, this is an article going through how JeringaIt's free 🔗 implements some it's methods.
using Jeringa;
public class DemoInjection : MonoBehaviour {
[FromSelf, SerializeField] Rigidbody selfRigidbody;
[FromType(typeof(MainPlayer))] Transform mainPlayerTransform;
[FromAny] AudioSource anyAudioSource;
[FromParent] CapsuleCollider parentCapsule;
[FromChild] Rigidbody childRigidbody;
[FromSibling] Collider2D[] siblingCollider2dArray;
void Awake() => Injector.Inject(this);
}Jeringa provides Custom Attributes that define from where that dependency will be injected from.
In this article I'll explain how you can implement [FromSelf] and [FromAny], the implementation of the other attributes is similar to these two.
The way this system is implemented means that only MonoBehavior/Component dependencies can be injected.
Some methods, like
[FromSelf]or[FromChild]can only be called on MonoBehavior classes, while others, such as[FromAny], can be called from any class, as this last method doesn't depend on a Scene Hierarchy.
What are C# Attributes?
C# Attributes essentially allow you to attach metadata to elements such as Fields or Classes. You can then process/parse this metadata using Reflection to for example, set the value of a field.
A common Unity attribute is the [Range] attribute, which tells Unity to draw a slider in the Inspector (for numeric fields).
You can also pass arguments to an attribute, in the example of the [Range] attribute you can pass a Min. and Max. values.
[Range(0f, 10f)]
public float speed = 5f;How do we make our own Attributes?
Before getting into using Attributes for Dependency Injection, let's implement a simpler system to log variables to the console with Attributes.
To get a custom attribute to work we need 3 main parts:
- Our Attribute class
- A method defining how to process those elements with the attribute
- A method to iterate through all elements that might have the attribute
Here's a simple implementation of an attribute that logs fields:
// 1
[AttributeUsage(AttributeTargets.Field)] // only attachable to fields
public class PrintAttribute : Attribute {
public bool printType = false; // custom argument for attribute
// 2
public void Process(MonoBehaviour instance, FieldInfo field) {
Debug.Log(
$"[{instance.name}] {field.Name}: {field.GetValue(instance)}" +
(printType ? $" - {field.FieldType}" : "")
);
}
// 3
// gather and process fields of instance
public static void ProcessAll(MonoBehaviour instance) {
Type type = instance.GetType();
// public, non-public, static and instance fields
FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static);
foreach (var field in fields) {
PrintAttribute attribute = field.GetCustomAttribute<PrintAttribute>(true);
if (attribute != null) attribute.Process(instance, field);
}
}
}Then we need a place to call our static ProcesAll() method. In this case we're going to call it in the Awake() method of a MonoBehavior, passing the instance to ensure we can properly call field.GetValue().
public class Player : MonoBehaviour {
[PrintAttribute] public string playerName;
[Print(printType = true)] int health = 100;
// ProcessAll takes the instance
void Awake() => PrintAttribute.ProcessAll(this);
}// After running this with two Player instances we get:
[PlayerA] playerName: Peter
[PlayerA] health: 100 - System.Int32
[PlayerB] playerName: John
[PlayerB] health: 100 - System.Int32A couple of things to note:
[PrintAttribute]can be simplified to[Print]- By defining a public field (
printType) inPrintAttribute, it becomes an argument
Basic Dependency Injection with Custom Attributes
Applying these concepts to a dependency injection system is pretty straightforward, first we'll define an abstract class with some methods, as we will want multiple Injection Attributes (FromSelf and FromAny.)
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public abstract class InjectAttribute : Attribute {
public abstract void Inject(object target, FieldInfo field);
protected void LogNullDependency(object target, Type fieldType) {
Debug.LogWarning($"[{this}] No object with a {fieldType} found for {target}");
}
}public class FromSelf : InjectAttribute {
public override void Inject(object target, FieldInfo field) {
MonoBehaviour self = (MonoBehaviour)target; // to call GetComponent
// fieldType indicates *what* we want to inject (RigidBody, Camera, etc...)
Type fieldType = field.FieldType;
// get dependency by calling GetComponent on self
object dependency = self.GetComponent(fieldType);
// Log Warning and return if dependency not found
if (dependency == null) {
LogNullDependency(target, fieldType);
return;
}
// fill field with dependency
field.SetValue(target, dependency);
}
}The implementation of FromAny is almost identical.
public class FromAny : InjectAttribute {
public override void Inject(object target, FieldInfo field) {
Type fieldType = field.FieldType;
object dependency = GameObject.FindAnyObjectByType(fieldType,
FindObjectsInactive.Include);
// ... null check ...
field.SetValue(target, dependency);
}
}Note that this implementation doesn't handle array dependencies, for that, feel free to check Jeringa's source code.
Similarly to the PrintAttribute, we define a public method that we call to process all the fields in a MonoBehavior.
public static class Injector {
public static void Inject(MonoBehaviour instance) {
Type type = instance.GetType();
FieldInfo[] fields = type.GetFields(BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance);
foreach (var field in fields) {
InjectAttribute attribute = field.GetCustomAttribute<InjectAttribute>(true);
if (attribute != null) attribute.Inject(instance, field);
}
}
}And then we can quickly setup our custom attributes to our Player class, and call Injector.Inject() whenever we need to fill our dependencies:
public class Player : MonoBehaviour {
[FromAny] Light sceneLight;
[FromSelf] MeshRenderer myMeshRenderer;
void Awake() => Injector.Inject(this);
}