Which type should you register in an IOC container?

  Subscribe
7/16/2018 - (updated on 7/16/2018)

Use Case

I just ran into an issue recently where a concrete implementation registered as a singleton was suddenly not registered as a singleton because of architectural changes.

The changes involved creating mini-applications within a main application, each of which has its own IOC. Instead of creating controllers using the main application, I was now creating controllers with the mini-application instead (to support multi-tenancy, of which more in an upcoming post).

Silent Replacement of Singleton with Transient

Controllers are, by their nature, transient; a new controller is created to handle each incoming request.

In the original architecture, the concrete singleton was injected into the controller and all controller instances used the same shared instance. In the new architecture, the registration was not present in the mini-application (at first), which led to a (relatively) subtle bug: a transient and freshly created instance was injected into each new controller.

In cases where the singleton is a stateless algorithm, this wouldn't be a logical problem at all. At the very worst, you're over-allocating---but you probably wouldn't notice that, either. In this case, the singleton was a settings object, configured at application startup. The configured object was still in the main application's IOC, but not registered in the mini-application's IOC.

Because the singleton was registered on a concrete type rather than an interface, the semantic error occurred silently instead of throwing a lifestyle-mismatch or unregistered-interface exception.

A Straightforward Fix

This is only one of the reasons that I recommend using interfaces as the anchoring type of an IOC registration.

To fix the issue, I did exactly this: I extracted an interface from the class and used the interface everywhere (except for the implementing type of the registration). Re-running the test caused an immediate exception rather than a strange data bug (which resulted because the default configuration in the concrete type was just correct enough to allow it to limp to a result).

To show an example, instead of the following,

application.RegisterSingle<ApiSettings>()

I used,

application.RegisterSingle<IApiSettings, ApiSettings>()

This still didn't fix the crash because the mini-application doesn't get that registration automatically.

I also can't use the same registration as above because that would just create a new unconfigured ApiSettings in each mini-application (the same as I had before, but now as a singleton). To go that route, I would have to replicate the configuration-loading for the ApiSettings as well. And I don't want to do that.

Instead, I just injected the IApiSettings from the main application to the component responsible for creating the mini-application and registered the object as a singleton directly, as shown below.

public class MiniApplicationFactory
{
  public MiniApplicationFactory([NotNull] IApiSettings apiSettings)
  {
    if (apiSettings = null) { throw new ArgumentNullException(nameof(apiSettings(); }

    _apiSettings = apiSettings;
  }

  IApplication CreateApplication()
  {
    return new Application().UseRegisterSingle(_apiSettings);
  }

  [NotNull]
  private readonly IApiSettings _apiSettings;
}

On a side note, whereas C# syntax has become more concise and powerful from version to version, I still think it has a way to go in terms of terseness for such simple objects. For such things, Kotlin and TypeScript nicely illustrate what such a syntax could look like.1

Other Drawbacks

I mentioned above that this is only "one" of the reasons I don't like registering concrete singletons. The other two reasons are:

  1. Complicates replacement: If the registered type is a concrete instance, then any replacement must inherit from this instance. The base class has to be constructed more carefully in order to allow for all foreseeable customizations. With an interface, the implementor is completely free to either use the existing class as a base or to re-implement the interface entirely.
  2. Limits Mocking: Related to the first reason is that mocking is limited in its ability to override non-virtual methods. Even without a mocking library, you're just as hard-pressed to work around unwanted behavior in a hand-coded mock as you are with an actual replacement (as described above). Such limitations are non-existent with interfaces.


  1. I'm still waiting for C# to clean up a bit more of this syntax for me. The [NotNull] should be a language feature checked by the compiler so that the ArgumentNullException is no longer needed. On top of that, I'd like to see parameter properties, as in TypeScript (this is where you can prefix a constructor parameter with a keyword to declare and initialize it as a property). With a few more C#-language iterations that included non-nullable reference types and parameter properties, the example could look like the code below:

    public class MiniApplicationFactory
    {
    public MiniApplicationFactory(private IApiSettings apiSettings)
    {
    }
    
    IApplication CreateApplication()
    {
      return new Application().UseRegistereSingle(apiSettings);
    }
    }
    

Sign up for our Newsletter