The IOC Container Anti-Pattern - Marcus Technical Services
Before I receive the Frankenstein-style lantern escort to the gallows, let me assure you: I love dependency injection (the basis of inversion of control). It is one of the core precepts of SOLID code design: _ one should “depend upon abstractions, not concretions _.” I also support real Inversion of Control – changing the _ flow _ of an application through a more complex form of dependency injection. Many modern frameworks borrow the term “IOC” to convince us that they are useful because, after all, why else would they be called that? Because they are selling the _ sizzle _: a way for programmers to do a thing without understanding it, or bothering with its design. This is how the IOC Container evolved. It often arrives as the cracker jacks toy inside of an MVVM Framework (https://marcusts.com/2018/04/06/the-mvvm-framework-anti-pattern/). It suffers from the same short-sightedness.
“Water, water everywhere… and not a drop to drink.”
― Not So Happy Sailor in Life Raft
These are Not “IOC” Containers At All
To qualify as a form of Inversion of _ Control _, these co-called “IOC Containers” would have to control something such as the app flow. All the containers do is to store global variables. In the more advanced containers, the programmer can insert constructor logic to determine how to create the variable that gets stored. That is _ not _ control. It is _ instantiation _ or _ assignment _. These entities should be called DI (”Dependency Injection”) containers. If we are to be taken seriously for our ideas, we should be careful not to exaggerate their features.
Global Variables are Bad Design
A so-called “IOC Container” is a dictionary of _ global variables _ which is generally accessible from _ anywhere _ inside of a program. This intrinsically violates C# coding principles. C# and SOLID require that class variables be _ as private as possible _. This keeps the program loosely coupled, since interaction between classes must be managed by interface contracts. Imagine if you handed this code to your tech lead:
public interface IMainViewModel { } public class MainViewModel : IMainViewModel { } public partial class App : Application { public App() { GlobalVariables.Add(typeof(IMainViewModel), () => new MainViewModel()); InitializeComponent(); MainPage = new MainPage() { BindingContext = GlobalVariables[typeof(IMainViewModel)] }; } public static Dictionary<Type, Func<object>> GlobalVariables = new Dictionary<Type, Func<object>>(); }
You would be _ fired _. “What are you thinking of?” , your supervisor demands. “Why not just create the MainViewModel
where it is needed, and keep it private?” Then you provide this code:
public interface IMainViewModel { } public class MainViewModel : IMainViewModel { } public static class AppContainer { public static IContainer Container { get; set; } } public partial class App : Application { public App() { var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); AppContainer.Container = containerBuilder.Build(); InitializeComponent(); MainPage = new MainPage() { BindingContext = AppContainer.Container.Resolve<IMainViewModel>()}; } public static IContainer IOCContainer { get; set; } }
You now receive a pat on the back. Brilliant! Except for a tiny issue: _ this is the same code _. Both solutions rely on a _ global static dictionary _ of variables. We don’t globalize any class variable in a program unless that variable must be readily available from anywhere. This might apply to certain services, but almost nothing else. Indeed, the precursor to modern IOC Containers is a “service locator”. That’s where it should have ended. Let’s refactor and expand the last example to add a second view model, which we casually insert into the IOC Container:
public static class AppContainer { static AppContainer() { var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); containerBuilder.RegisterType<SecondViewModel>().As<ISecondViewModel>(); Container = containerBuilder.Build(); } public static IContainer Container { get; set; } }
Inside the second page constructor, we make a mistake. We ask for the wrong view model:
public partial class SecondPage : ContentPage { public SecondPage() { BindingContext = AppContainer.Container.Resolve<IMainViewModel>(); InitializeComponent(); } }
Oops! Why are we allowed to do that? Because all of the view models are global
, so can be accessed – correctly or incorrectly – from _ anywhere, by any consumer, for any reason _. This is a classic anti-pattern: a thing you should generally not do.
IOC Containers Are Not Compile-Time Type Safe
This actually compiles, even though the SecondViewModel
does *not* implement IMainViewModel
.
containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); containerBuilder.RegisterType<SecondViewModel>().As<IMainViewModel>();
At run-time, it crashes! The goal and responsibility of all C# platforms is to produce _ compile-time type-safe _ interactions. Run-time is extremely unreliable in comparison.
IOC Containers Create New Instances of Variables By Default
Quick quiz: is this equality test true?
var firstAccessedMainViewModel = AppContainer.Container.Resolve<IMainViewModel>(); var secondAccessedMainViewModel = AppContainer.Container.Resolve<IMainViewModel>(); var areEqual = ReferenceEquals(firstAccessedMainViewModel, secondAccessedMainViewModel);
The answer is _ no _, it is _ false _. The container routinely issues a separate instance every variable requested. This is shocking, since most variables must maintain their state during run-time. Imagine creating a system settings view model:
containerBuilder.RegisterType<SettingsViewModel>().As<ISettingsViewModel>();
You need this view model in two locations: at the profile (where the settings are stored) and the main page and its view model (where they are consumed).
var settingsViewModel = AppContainer.Container.Resolve<ISettingsViewModel>();
So we create the same dilemma just cited:
At Settings:
var settingsViewModel = AppContainer.Container.Resolve<ISettingsViewModel>();
At Main:
var settingsViewModel = AppContainer.Container.Resolve<ISettingsViewModel>();
The user opens a menu, goes to their profile, and changes one of the settings. They close that window, close the menu, and look at their main screen. Is the change visible? No! It’s stored in another variable. The main settings variable is now “stale”, so the main screen reflects incorrect values. There is an official hack for this. Instead of:
containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>(); containerBuilder.RegisterType<SecondViewModel>().As<ISecondViewModel>();
We write:
containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>().SingleInstance(); containerBuilder.RegisterType<SecondViewModel>().As<ISecondViewModel>().SingleInstance();
The SingleInstance
extension guarantees that the same instance of the variable will always be returned. The exception to this guidance is a list of lists:
public interface IChildViewModel { } public class ChildViewModel : IChildViewModel { } public interface IParentViewModel { IList<IChildViewModel> Children { get; set; } } public class ParentViewModel : IParentViewModel { public IList<IChildViewModel> Children { get; set; } }
The only way for the ParentViewModel
to add a list of children is to set their view models _ uniquely _. So in this case, the registration will be:
containerBuilder.RegisterType<ChildViewModel>().As<IChildViewModel>();
We do _ not _ include the SingleInstance()
suffix.
IOC Containers Instantiate Classes without Flexibility or Insight
Programmers learn to _ decouple _ classes to reduce inter-reliance (”branching”) with other classes. But this does not mean that we seek to give up control. A class can be instantiated and it can be destroyed. Instantiation is important because it is the where all forms of dependency injection take place. The IOC Container steals this control from us. The IOC Container analyzes the constructor of each store class to determine how to create an instance. It seeks the path of least resistance to building a class. But this does not guarantee an intelligent or predictable decision. For instance, these two classes share the same interface, but set the interface’s Boolean to different values:
public interface ICanBeActive { bool IsActive { get; set; } } public interface IGeneralInjectable : ICanBeActive { } public class FirstPossibleInjectedClass : IGeneralInjectable { public FirstPossibleInjectedClass() { IsActive = true; } public bool IsActive { get; set; } } public class SecondPossibleInjectedClass : IGeneralInjectable { public SecondPossibleInjectedClass() { IsActive = false; } public bool IsActive { get; set; } }
In order for the classes to be considered for injection, we have to add them “as” IGeneralInjectable
:
containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IGeneralInjectable>(); containerBuilder.RegisterType<SecondPossibleInjectedClass>().As<IGeneralInjectable>();
Notice that the classes are otherwise identical, and that their constructors also match exactly. Here is a class that receives an injection of only _ one _ of those two classes:
public class ClassWithConstructors { public bool derivedIsActive { get; set; } public ClassWithConstructors(IGeneralInjectable injectedClass) { derivedIsActive = injectedClass.IsActive; } } containerBuilder.RegisterType<ClassWithConstructors>();
Now we ask the IOC Container for an instance of ClassWithConstructors
:
var whoKnowsWhatThisIs = AppContainer.Container.Resolve<ClassWithConstructors>();
So how would an IOC Container decide what to inject to create ClassWithConstructors
? When the container checks the candidates for IGeneralInjectable
, it will find two candidates:
FirstPossibleInjectedClass
SecondPossibleInjectedClass
Both classes are parameterless, so that makes them equal. The IOC Container will pick the first one it can find. Whichever that one is, it will be _ wrong _. That’s because the two classes make a different decision for IsActive
. Since both are legal, and only one can be allowed, the IOC Container cannot be trusted with this decision. It should produce a _ compiler error _. But the “black box” logic inside the IOC Container masks this, and issues a result that we cannot rely on. It might surprise you just which class “won out” in this contest. It was the _ last class added _ to the container! I verified this by reversing the order in which they were added, and sure enough, the injection followed. There are other issues. Interfaces are flexible contracts, and a class can implement any number of them. For each new interface implemented, the class *must* declare this using the “as” convention, or it won’t work:
public interface IOtherwiseInjectable { } public interface IGeneralInjectable : ICanBeActive { } public class FirstPossibleInjectedClass : IGeneralInjectable, IOtherwiseInjectable { public FirstPossibleInjectedClass() { IsActive = true; } public bool IsActive { get; set; } } containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IGeneralInjectable>(); containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IOtherwiseInjectable>();
This can be done manually, of course. But what if there are dozens? What if you miss one? The container could mis-inject without any warnings or symptoms.
Takeaways
IOC Containers are an anti-pattern because:
- They are not at all IOC; they are dependency injection toys;
- They create global variables when none are needed;
- They are “too accessible” – in a class C# application, privacy rules the day. We don’t want everything to have access to everything else.
- They issue new instances of variables that should almost always be singletons;
- They leverage hyper-simplistic logic in instantiating classes that does not support the level of complexity and nuance present in most C# applications.
Hard Proofs
I created a Xamarin.Forms
mobile app to demonstrate the source code in this article. The source is available on GitHub at https://github.com/marcusts/xamarin-forms-annoyances. The solution is called <strong>IOCAntipattern.sln</strong>
.
I also designed a much smarter — and tiny! — DI Container, available for free at https://github.com/marcusts/Com.MarcusTS.SmartDI.
All code is published as open source and without encumbrance.
Any chance of increasing the height for code snippets? Really hard to follow the article with all the code in single lines