mtti.Inject is a simple dependency injector for the Unity engine.
Written for my personal use because I wanted to better understand how dependency injection frameworks work. For your own projects you should consider a more established framework like Zenject.
No UPM package is currently available, so you have to either add this library as a dependency to you project's manifest.json
using the GitHub URL:
"com.mattihiltunen.inject": "https://github.com/mtti/inject.git",
OR alternatively if you are using Git you can add this repository as a Git submodule under Packages/
:
git submodule add git@github.com:mtti/inject.git Packages/com.mattihiltunen.inject
then,
- Add the
mtti.Inject.DependencyInjector
MonoBehaviour to an empty GameObject, or to your existing "main" GameObject if you have one. - Implement services as plain C# classes (not as MonoBehaviours or ScriptableObjects) and mark them with
mtti.Inject.ServiceAttribute
. - Add a method to your MonoBehaviours to receive dependencies. You can call the methods whatever you want and they don't need to be
public
either. Just mark them withmtti.Inject.InjectAttribute
. - If you create new GameObjects programmatically (from prefabs with
Instantiate
, for example) inject dependencies into them manually.
Add the Dependency Injector component to an empty GameObject. Take care not to let multiple dependency injectors exist at the same time as the results of doing so are undefined.
Services are normal C# classes marked with the Service attribute. An instance of the class is created automatically the first time it's required.
// ExampleService.cs
using System;
using mtti.Inject;
[Service]
public class ExampleService
{
public int Sum(int a, int b)
{
return a + b;
}
}
MonoBehaviours and other services get dependencies injected directly into any fields marked with the [Inject]
attribute:
// ExampleScript.cs
using System;
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
[Inject]
private ExampleService _exampleService;
}
Method injection is also supported:
// ExampleScript.cs
using System;
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
private ExampleService _exampleService;
[Inject]
private OnInject(ExampleService exampleService)
{
_exampleService = exampleService;
}
}
The injection method gets called after field injection even if it has no parameters so you can use it as a callback to initialize objects after they've received their dependencies:
// ExampleScript.cs
using System;
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
[Inject]
private ExampleService _exampleService;
[Inject]
private OnInject()
{
this.gameObject.SetActive(true);
}
}
Normally, the [Inject]
attribute throws an exception when the dependency is unmet. If this is undesirable, [InjectOptional]
instead leaves the value of the field untouched and doesn't throw an exeception.
// ExampleScript.cs
using System;
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
[InjectOptional]
private ExampleService _exampleService;
[Inject]
private OnInject()
{
if (_exampleService != null)
{
this.gameObject.SetActive(true);
}
}
}
The Dependency Injector automatically injects dependencies into all GameObjects in the all loaded scenes when it starts and into all scenes that are loaded after the EntryPoint was created, but it can't magically detect when new GameObjects are created programmatically.
Therefore, you need to manually inject dependencies into new GameObjects you create. You do so using the overloaded Inject
method of the Injector
or UnityInjector
classes.
Any bool
field with the [Inject]
attribute is always injected the value true
. You can use this to easily check if dependency injection has taken place.
// ExampleScript.cs
public class ExampleScript : MonoBehaviour
{
[Inject]
private bool _wasInjected;
private void Update()
{
if (!_wasInjected_) return;
// ...
}
}
When using UnityInjector, the injector itself can be injected as a dependency.
// ExampleSpawner.cs
public class ExampleSpawner : MonoBehaviour
{
public GameObject EnemyPrefab;
[Inject]
private UnityInjector _injector;
public void SpawnNewEnemy()
{
var obj = (GameObject)Instantiate(EnemyPrefab);
_injector.Inject(obj);
return obj;
}
}
mtti.Inject includes some Unity-specific helper attributes which don't have anything to do with dependency injection directly, but should help with reducing repetitive boilerplate code when finding references to GameObject components.
[GetComponent]
mirrors the functionality of UnityEngine.Component.GetComponent.
Instead of doing this:
// ExampleScript.cs
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
private MeshCollider _meshCollider;
[Inject]
private void OnInject()
{
_meshCollider = GetComponent<MeshCollider>();
}
}
You can do this:
// ExampleScript.cs
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
[GetComponent]
private MeshCollider _meshCollider;
}
[GetComponentInChildren]
mirrors the functionality of UnityEngine.Component.GetComponentInChildren.
Instead of doing this:
// ExampleScript.cs
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
private MeshCollider _meshCollider;
[Inject]
private void OnInject()
{
_meshCollider = GetComponentInChildren<MeshCollider>();
}
}
You can do this:
// ExampleScript.cs
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
[GetComponentInChildren]
private MeshCollider _meshCollider;
}
[EnsureComponent]
is the same as GetComponent, except it adds the component if it doesn't already exist. Instead of doing this:
// ExampleScript.cs
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
private MeshCollider _meshCollider;
[Inject]
private void OnInject()
{
_meshCollider = GetComponent<MeshCollider>();
if (_meshCollider == null)
{
_meshCollider = this.gameObject.AddComponent<MeshCollider>();
}
}
}
You can do this:
// ExampleScript.cs
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
[EnsureComponent]
private MeshCollider _meshCollider;
}
Services can use the [Inject]
attribute on one of their methods to require other services. Be aware, though, that circular dependencies are not supported and will fail throw a DependencyInjectionException
.
In the basic example, ExampleService was marked with [Service]
with no parameters. This binds the service using its concrete class, ExampleService. You can however bind a service with an interface that it implements by passing the interface type as a parameter to the Service attribute.
This makes it possible to create mock services for unit testing.
ExampleService could be written as:
// ExampleService.cs
using System;
using mtti.Inject;
public interface IExampleService
{
int Sum(int a, int b);
}
[Service(typeof(IExampleService))]
public class ExampleService
{
public int Sum(int a, int b)
{
return a + b;
}
}
After which it can be injected using the interface:
// ExampleScript.cs
using System;
using UnityEngine;
using mtti.Inject;
public class ExampleScript : MonoBehaviour
{
private IExampleService _exampleService;
[Inject]
private void OnInject(IExampleService exampleService)
{
_exampleService = exampleService;
}
}
Normally a service instance is created using a parametreless default constructor, but if you want a lazily initialized service while still having some more control over how it's initialized, you can use a static factory method.
[Service]
private static IExampleService ExampleServiceFactory()
{
return new ExampleService();
}
One situation where you might want to do this is if you want to have a generic service. C# doesn't allow generic types in attribute parameters so you can't do [Service(typeof(MyGenericService<string>))]
, for example.
Another use case is when you want a service to exist as a GameObject in your Unity scene so you can see it in the hierarchy and inspector.
using UnityEngine;
using mtti.Inject;
public class MyService : MonoBehaviour
{
[Service]
private static MyService FindMyService()
{
return (MyService)UnityEngine.Object.FindObjectOfType(typeof(MyService));
}
public int Sum(int a, int b)
{
return a + b;
}
}
You can bind dependencies manually to an injector instance. For example, you could do injector.Bind<IExampleService>(new ExampleService());
.
You can create instances of mtti.Inject.Injector (the base class) and mtti.Inject.UnityInjector (Unity-specific subclass) normally, for example when writing unit tests or just to have full control over when and how dependencies are injected.
The Injector.Invoke()
overloads allow you to dynamically invoke arbitrary
static and instance methods with a combination of manually set and automatically
injected parameters.
This gives up compile-time type safety, uses reflection and allocates a bit of garbage on each call so use sparingly.
// SomeClass.cs
using System;
using mtti.Inject;
public class SomeClass
{
public int SomeMethod(int a, int b, IExampleService example)
{
// ...
return a + b;
}
}
// Somewhere else
var someInstance = new SomeClass();
var result = injector.Invoke<int>(
someInstance,
"SomeMethod",
new List<object>(new object[] { 23, 42 })
);
// result = 23 + 42