Jeringa: Custom C# Attributes for Easy Dependency Injection

2026-01-19, Game Development

Jeringa Logo

Asset Store | GitHub

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:

  1. Our Attribute class
  2. A method defining how to process those elements with the attribute
  3. A method to iterate through all elements that might have the attribute

Here's a simple implementation of an attribute that logs fields:

PrintAttribute.cs
// 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().

Player.cs
public class Player : MonoBehaviour {
    [PrintAttribute] public string playerName;
    [Print(printType = true)] int health = 100;
 
    // ProcessAll takes the instance
    void Awake() => PrintAttribute.ProcessAll(this);
}
Output
// After running this with two Player instances we get:
[PlayerA] playerName: Peter
[PlayerA] health: 100 - System.Int32
[PlayerB] playerName: John
[PlayerB] health: 100 - System.Int32

A couple of things to note:

  • [PrintAttribute] can be simplified to [Print]
  • By defining a public field (printType) in PrintAttribute, 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.)

InjectAttribute.cs
[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}");
    }
}
InjectAttribute.cs
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.

InjectAttribute.cs
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.

InjectAttribute.cs
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:

Player.cs
public class Player : MonoBehaviour {
    [FromAny] Light sceneLight;
    [FromSelf] MeshRenderer myMeshRenderer;
 
    void Awake() => Injector.Inject(this);
}