Introduction
After using Unity3d for some time, I was looking up if there is any IoC-Container-Framework implementation that could be used. I stumbled over the implementation by Sebastiano MandalĂ that can be found on his blog here. While this is a very good Framework, I was not totally satisfied by it, because for my small projects, the implementation was too abstract and was split in too many components. Also, coming from the Microsoft world, I often used the IoC-Container-Framework from Prism.Unity for WPF/ASP.NET projects and therefore am accustomed to their syntax.
So I started to implement my own IoC-Container for Unity3d that has a similar syntax as Prism.Unity
, based on some ideas of the implementation of Sebastiano MandalĂ . If you like a more compact solution and/or are familiar with Prism.Unity
, this might be the right framework for you to start with.
About the Implementation
Interfaces
The IoC container implements some interfaces that show the different tasks of the IoC-Container:
IIoCContainer
- Holds information of registered types
IServiceLocator
- Can locate a registered implementation of a given type
IDependencyInjector
- Can inject known dependencies into fields or properties of a given object
public interface IIoCContainer
{
void Register<T>() where T : class;
void Register<TInterface, TClass>(string key = null) where TClass : class, TInterface;
void RegisterSingleton<T>() where T : class;
void RegisterSingleton<T>(T instance, string key = null) where T : class;
void RegisterSingleton<TInterface, TClass>(string key = null) where TClass : class, TInterface;
}
public interface IServiceLocator
{
T Resolve<T>(string key = null) where T : class;
object Resolve(Type type, string key = null);
}
public interface IDependencyInjector
{
object Inject(Type type, object obj);
T Inject<T>(object obj);
}
The Data Class
The container uses the following class to hold the information of a registered type. The class has a factory method called Create
that gathers and stores all public
fields and properties with the [Dependency]
attribute set.
private class TypeData
{
public object Instance { get; set; }
public List<KeyValuePair<DependencyAttribute, PropertyInfo>> Properties { get; private set; }
public List<KeyValuePair<DependencyAttribute, FieldInfo>> Fields { get; private set; }
public bool IsSingleton { get; private set; }
private TypeData()
{
this.Properties = new List<KeyValuePair<DependencyAttribute, PropertyInfo>>();
this.Fields = new List<KeyValuePair<DependencyAttribute, FieldInfo>>();
}
public static TypeData Create(Type type, bool isSingleton = false, object instance = null)
{
var typeData = new TypeData { IsSingleton = isSingleton, Instance = instance };
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
var dependency =
(DependencyAttribute)field.GetCustomAttributes(typeof(DependencyAttribute), true).FirstOrDefault();
if (dependency == null) continue;
typeData.Fields.Add(new KeyValuePair<DependencyAttribute, FieldInfo>(dependency, field));
}
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var dependency =
(DependencyAttribute)property.GetCustomAttributes(typeof(DependencyAttribute), true).FirstOrDefault();
if (dependency == null) continue;
typeData.Properties.Add(new KeyValuePair<DependencyAttribute, PropertyInfo>(dependency, property));
}
return typeData;
}
}
The Container
The container needs two dictionaries. One to connect interfaces to their implementations and the other to connect the implementations with their type data.
private readonly Dictionary<Type, Dictionary<string, Type>>
types = new Dictionary<Type, Dictionary<string, Type>>();
private readonly Dictionary<Type, TypeData> typeDatas = new Dictionary<Type, TypeData>();
These dictionaries are used in the private Register
method to store the type information.
private void Register(Type interfaceType, Type type, TypeData typeData, string key = null)
{
try
{
if (this.types.ContainsKey(interfaceType ?? type))
{
this.types[interfaceType ?? type].Add(key ?? string.Empty, type);
}
else
{
this.types.Add(interfaceType ?? type, new Dictionary<string, Type> { { key ?? string.Empty, type } });
}
this.typeDatas.Add(type, typeData);
}
catch (Exception ex)
{
throw new IoCContainerException("Register type failed.", ex);
}
}
After registering your classes, the types can be resolved. If the type to resolve is derived from MonoBehavior
, the script is created through calling AddComponent
on a GameObject
.
public object Resolve(Type type, string key = null)
{
Guard(!this.types.ContainsKey(type), "The type {0} is not registered.", type.Name);
Guard(!this.types[type].ContainsKey(key ?? string.Empty),
"There is no implementation registered with the key {0} for the type {1}.", key, type.Name);
var foundType = this.types[type][key ?? string.Empty];
var typeData = this.typeDatas[foundType];
if (foundType.IsSubclassOf(typeof(MonoBehaviour))) {
Guard(this.singletonGameObjectName == null,
"You have to set a game object name to use for MonoBehaviours with SetSingletonGameObject() first.");
var gameObject = GameObject.Find(this.singletonGameObjectName)
?? new GameObject(this.singletonGameObjectName);
return gameObject.GetComponent(type.Name) ?? Inject(foundType, gameObject.AddComponent(foundType));
}
if (typeData.IsSingleton)
{
return typeData.Instance ?? (typeData.Instance = this.Setup(foundType));
}
return this.Setup(foundType);
}
Using the Code
IoC-Container Setup
To set up the IoC-Container, a few step have to be made. To give an example, I will show you how to setup a Unity3d Project with it.
First, create a new Unity3d-Project and copy the Framework folder into the Assets folder:
Next, you have to implement the AbstractBootstrapper.cs. This class will be the entry point for the IoC-Container. You have to override the Configure
method, where you put all your services/components you want to register.
public class Bootstrapper : AbstractBootstrapper
{
public override void Configure(IIoCContainer container)
{
container.Register<IColorItem, ColorItem>();
container.RegisterSingleton<IColorFactory, RedColorFactory>("red");
container.RegisterSingleton<IColorFactory, GreenColorFactory>("green");
container.RegisterSingleton<IColorFactory, BlueColorFactory>("blue");
container.RegisterSingleton<IColorHistory, ColorHistory>();
}
}
Next, create an empty GameObject
and give it a name (e.g., Container
) and add the Bootstrapper as Component:
Now, we need to force Unity to execute the Bootstrapper
as the first MonoBehaviour
at application start. This is done as the following:
Left-click on any script in your project to bring up the following view in the inspector and click "Execution Order...":
The inspector view will change. Click the plus-icon and select the Bootstrapper
. Done.
Injecting Dependencies into MonoBehaviour Scripts
If you want to inject your dependencies, all you need to do is to mark the corresponding public
properties or fields with the [Dependency]
attribute and call the method Inject
in Start
. The Inject
method is an extension method that comes with the IoC-Container implementation.
public class ColorDropper : MonoBehaviour
{
[Dependency("red")] public IColorFactory RedColorFactory;
[Dependency("blue")] public IColorFactory BlueColorFactory;
[Dependency("green")] public IColorFactory GreenColorFactory;
void Start () {
this.Inject();
this.StartCoroutine(this.DropColor());
}
Points of Interest
Example Project
The example project you can download shows only how to use the container in different ways. The implementation of the example itself may not be the way it would be actually done.
My Opinion
What I like the most about using an IoC-Container in Unity is, that it gives you a clearer guide on how you get to components of other GameObjects
. You don't need to think about using ways like GameObject.Find
(to mention the most inefficient way) to get to them.
It also shows you very clearly what your scripts depend on.