Dependency Injection with Unity - Part 3

Table of Contents

In part 2 I showed how to bootstrap a Unity project using the dependency injection pattern. I also created a very simple solution for automating the bootstrap process.

In this article I will address some of the shortcomings of this solution. In addition I will show how unit tests are much easier if your components adhere to the principles of dependency injection.

Reduce boilerplate with assembly scanning

If you have just a small project with just a handful of components, using the Declare method of DependencyContext is just fine. But as the project grows so does your list of Declare-invocations. For every new component you need a new Declare-call and this is not very DRY. So how can you reduce this boilerplate?

If you mark every component with an attribute, then you can easily whip together an assembly scanner that finds all your components and automatically declares them. So lets start with the attribute:

public class DependencyComponentAttribute : Attribute
{
}

Now you can annotate your component with this attribute:

[DependencyComponent]
public class MyComponent {
  public MyComponent(MyOtherComponent otherComponent, 
      YetAnotherComponent yetAnotherComponent) {
  }
}

And finally you can build a simple assembly scanner that looks for all component types that are annotated with [DependencyComponent] and automatically calls Declare for each type:

public static void DeclareAnnotatedComponents(
    this DependencyContext context) {
            
  foreach (var type in AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(it => it.GetTypes())
    .Where(
      it.GetCustomAttribute(
         typeof(DependencyComponentAttribute)) != null)
    ) {
    context.DeclareQualified(type, qualifier);
}

This is defined as an extension method to DependencyContext, so you can easily mix auto-scanned and hand-declared components in your bootstrapping code:

var fromScene = FindObjectOfType<MyBehaviour>();
// declare the component from the current scene
context.Declare(fromScene);

// declare all components with the [DependencyComponent] attribute
context.DeclareAnnotatedComponents();

dependencyContext.Resolve();

So from now on when you create a new component you just need to add the [DependencyComponent] attribute and the component will automatically be declared without additional boilerplate code.

Dependencies of objects with their own lifecycle

So far DependencyContext can handle components which have their own lifecycle as long as these objects don’t have dependencies on their own. So it’s ok to inject a Button (which is a MonoBehaviour) into some non-MonoBehaviour object. But what if a MonoBehaviour itself has dependencies?

Constructor parameters do not help here, first and foremost because Unity does its own instantiation of MonoBehaviours and therefore you cannot have constructors with parameters for them. A second aspect is that the component is already constructed by Unity before your dependency injection solution sees it.

So the only thing you can do with these type of components is to late-initialize them. An easy way to do this is to provide a method in your MonoBehaviour which takes all required parameters, so you can properly initialize the object with a single method call instead of manually filling properties and forgetting one in the process. As an example, lets add some Eraser to our “game”. The Eraser will randomly move through the scene and delete all players he collides with.

To do this, it will call a method of PlayerService. Of course in this simple example, the Eraser could simply call GameObject.Destroy but let’s just assume that when destroying a player some cleanup would need to be made and therefore it is just better to use a service to properly destroy a player. So Eraser is a MonoBehaviour and needs a PlayerService. You could write some LateInit method to get hold of the PlayerService and then call the DropPlayer method of PlayerService when the Eraser collides with a player:

public class Eraser : MonoBehaviour {
  private PlayerService _playerService;
    
  public void LateInit(PlayerService playerService) {
    _playerService = playerService;
  }
    
  private void OnTriggerEnter(Collider other)
  {
    if (other.gameObject.CompareTag("Player"))
    {
      _playerService.DropPlayer(other.GetComponent<Player>());
    }
  }
}

If you were to manually bootstrap this, you could fetch the Eraser from the scene and initialize it by calling the LateInit method:

var eraser = (Eraser) FindObjectOfType(typeof(Eraser));
eraser.LateInit(playerService);

Integrating this with the automated bootstrapping from part 2 isn’t hard either, but it requires some change of the internals. The Declare method which takes a pre-made component can no longer assume that the pre-made component is fully initialized yet. So during the Resolve phase, the DependencyContext needs to inspect pre-made components to determine if they have a LateInit method and call this method with the appropriate arguments. This inspection is actually very similar to the one done when invoking the constructor. I will not print all the code changes for this in this article, so if you are interested in the code have a look at the final implementation of DependencyContext.

More than one component of a single type

For many cases it is sufficient to have one component per type. E.g. you only need one PlayerService and one ConfigService. Sometimes though, having more than one component of the same type can be useful. For example you might want to have a service that allows you to show a dialog in your UI (this is also known as a view model in MVVM terms). It takes a RectTransform which is the dialog root and references to two Buttons which resemble the Ok and Cancel options of a dialog:

public class DialogUIService {
  public DialogUIService(RectTransform dialog, 
        Button okButton, Button cancelButton) {
    // ...
  }
}

Manually bootstrapping this is not a problem at all:

var dialog = Resources
  .FindObjectsOfTypeAll<RectTransform>()
  .First(it => it.name == "Dialog");

var buttons = Resources.FindObjectsOfTypeAll<Button>();
var okButton = buttons.First(it => it.name == "OkButton");
var cancelButton = buttons.First(it => it.name == "CancelButton");

var dialogUiService = 
  new DialogUiService(dialog, okButton, cancelButton);

The automated bootstrapper in DependencyContext will have a problem with this though if you declare buttons and the dialog like this:

var buttons = Resources.FindObjectsOfTypeAll<Button>();
context.Declare(
    buttons.First(it => it.name == "OkButton"));
context.Declare(
    buttons.First(it => it.name == "CancelButton"));
context.Declare<DialogUIService>();

You declare two buttons and the constructor of DialogUIService takes two arguments of type Button. But which button should go into which argument of the constructor? When you bootstrap manually, this is all totally clear. But the automated bootstrapper needs some additional hint to know which button should go into which constructor argument. A way of solving this, is to mark the parameters with an attribute that assigns a sort of tag to the parameter. I’m calling this attribute Qualifier, other DI solutions may use different names (e.g. Zenject calls it ‘Id’).

public class DialogUIService {
  public DialogUIService(RectTransform dialog, 
        [Qualifier("OkButton")] Button okButton, 
        [Qualifier("CancelButton")] Button cancelButton) {
    // ...
  }
}

Now you need to modify the DependencyContext to support qualifiers. The first thing you need is a way of declaring a component with a qualified name. So let’s add a method for this:

public class DependencyContext {
  // ...
  public void DeclareQualified<T>(string qualifier) {
    //...
  }
    
  public void DeclareQualified<T>(string qualifier, T component) {
    //...
  }
}

Just like with the normal Declare there are two variants. In one variant you just give the type and qualifier and DependencyContext will create an instance of the component itself using the constructor of the component. In the second variant, you give the qualifier and a pre-instantiated component (e.g. a MonoBehaviour). With this new method you can now Declare the buttons under their qualified name and DependencyContext has a chance to know which buttons should go into which constructor parameter of DialogUIService. You can even simplify the code and just Declare the buttons under the name of their GameObject:

Resources
  // find all buttons
  .FindObjectsOfTypeAll<Button>()
  .ToList()
  .ForEach(
    button => 
      // declare each button under the name of its
      // game object.
      context.DeclareQualified (
          button.gameObject.name, button
      )
    );

// ...

dependencyContext.Resolve();

For this to actually work, you will need to quite substantially change the internal bookkeeping of DependencyContext. For each component that is declared DependencyContext will now need to track qualifiers and when resolving parameters these qualifiers will need to be properly resolved. It’s not really complicated, but showing all the code here would make an already long article even longer. So if you would like to know how it could be done, I again invite you to read the source code of the final implementation of DependencyContext.

Unit testing with dependency injection

Dependency injection is not only useful when bootstrapping your project, it is also very helpful when doing unit tests. Let’s quickly write a unit test for the configuration service:

public class ConfigurationServiceTest {
  private ConfigurationService _underTest;
  private Configuration _configuration;
  
  [SetUp]
  public void SetUp() {
    _configuration = new Configuration();
    _underTest = new ConfigurationService(_configuration);
  }
  
  [Test]
  public void PlayerConfigurationIsProvidedProperly()
  {
    // when
    var playerConfig = _underTest.GetPlayerSpawnConfiguration();
      
    // then  
    Assert.AreEqual(_configuration.playerPrefab, playerConfig.PlayerPrefab);
    Assert.AreEqual(_configuration.initialPlayerHealth, playerConfig.InitialPlayerHealth);
    Assert.AreEqual(_configuration.initialPlayerStrength, playerConfig.InitialPlayerStrength);
  }
}

This test verifies that the configuration service properly extracts the player spawn configuration from the configuration object. In the SetUp method you can see the benefits of using the dependency injection pattern. Because all dependencies are constructor arguments you can immediately see which dependencies the ConfigurationService has. You don’t need to inspect ConfigurationService’s code to find out how to initialize it. You can invoke the constructor and your friendly IDE of choice will show you the constructor arguments in code completion.

Another benefit is, that you have full control over the bootstrapping process of your tests. If you had a service locator or singleton pattern, the object under test could pull its dependencies from everywhere and those dependencies would then pull their dependencies as well. This way you could end up spawning half of your game to do a single unit test. While this is not a problem per se, it introduces all kinds of side effects and therefore complexity into your tests. In addition it makes tests run longer than they need to run.

When using the dependency injection pattern though, you are in control of what is being created. You can decide how many dependencies get instantiated. You can also give your component under test a test double as dependency if the real thing is too expensive or too complicated to set up. So let’s assume that your configuration object is really complicated to set up and you don’t want to do this in your test.

  [SetUp]
  public void SetUp() {
    // use NSubstitute to replace expensive configuration
    // setup with a test double.
    _configuration = Substitute.For<Configuration>();
    // you still need to set up your test double here
    // ...
    _underTest = new ConfigurationService(_configuration);
  }

If you are using a service locator this becomes more difficult as you need to patch up your service locator to return a test double instead of the real configuration. In addition you first need to look into ConfigurationService’s code to find out what dependencies it fetches from the service locator. If you were using singletons, you would have no way of getting a test double into your ConfigurationService.

Summary

  • Bootstrapping an application using the dependency injection pattern is straightforward but somewhat tedious. It therefore is a good target for automation. You can use an assembly scanner to find all your components, so you don’t have to manually declare and wire up each component.
  • Components with a lifecycle outside of your control will need to be late-initialized. You can do this by providing a LateInit method which takes all required dependencies as arguments (like a constructor would). Then you can call this method when you resolve your dependencies.
  • Having your components conform to the dependency injection pattern is very helpful for unit testing them. You have full control over the bootstrapping of components in a unit test. In addition you see all required dependencies at a glance when instantiating the component under test without having to look at the implementation.

If you would like to use the DependencyContext class as a starting point for your own projects, I have extracted it out into a separate repository called Nanoject. There is also an extension called for working with MonoBehaviours.