Click here to Skip to main content
14,539,639 members

The IOC Container Anti-Pattern

Rate this:
4.42 (15 votes)
Please Sign up or sign in to vote.
4.42 (15 votes)
14 Apr 2018CPOL
Before I receive the Frankenstein-style lantern escort to the gallows, let me assure you: I love dependency injection.... The post The IOC Container Anti-Pattern appeared first on Marcus Technical Services..

The IOC DI Container Anti-Pattern

So Much IOC; So Little Inversion or Control

 

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).

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.

IOC Containers do Not Manage Class Lifecycle Properly

The destruction of a class is also important because the class has gone out of scope, so should be disposed by the C# run-time environment.  That frees up memory and guarantees that the class will not interfere with a program where it has lost its role.

One of the reasons that we do not declare a lot of global variables is because their lifespan is forever.  The app never goes away.  Whatever is bootstrapped or directly declared in app.xaml.cs is a permanent fixture.  So in the current examples:

public static class AppContainer
{
	static AppContainer()
	{

		var containerBuilder = new ContainerBuilder();
		containerBuilder.RegisterType<ParentViewModel>().As<IParentViewModel>().
		SingleInstance();
		containerBuilder.RegisterType<ChildViewModel>().As<IChildViewModel>();
		containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>().SingleInstance();
		containerBuilder.RegisterType<SecondViewModel>().As<ISecondViewModel>().
		SingleInstance();
		containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IGeneralInjectable>();
		containerBuilder.RegisterType<FirstPossibleInjectedClass>().As<IOtherwiseInjectable>();
		containerBuilder.RegisterType<SecondPossibleInjectedClass>().As<IGeneralInjectable>();
		containerBuilder.RegisterType<ClassWithConstructors>();
		Container = containerBuilder.Build();
	}

	public static IContainer Container { get; set; }
}

.. everything here will survive from app startup to app shutdown.  That is not their purpose.  The view models are only needed as long as their accompanying views are visible.

IOC Containers have responded by adding a “scope” to the request for a class instance:

public partial class SecondPage : ContentPage
{
	public SecondPage()
	{
		using (var scope = AppContainer.Container.BeginLifetimeScope())
		{
			BindingContext = scope.Resolve<ISecondViewModel>();
		}

		InitializeComponent();
	}
}

Problem solved, right?  Let’s test it.  Here is a new version of the second page view model, which we set as our BindingContext above:

public class SecondViewModel : ISecondViewModel
{
	private bool _isAlive;

	public SecondViewModel()
	{
		_isAlive = true;

		Device.StartTimer(TimeSpan.FromSeconds(1), () =>
		{
			if (_isAlive)
			{
				Debug.WriteLine("Second View Model is still alive");
			}

			return _isAlive;
		});
	}

	// The only way to determine if this object is being garbage-collected
	~SecondViewModel()
	{
		_isAlive = false;
		Debug.WriteLine("Second View Model is being garbage collected");
	}
}

As long as the view model is alive, we write to the console.  We also add a finalizer check to see if the view model is ever placed for garbage collection.

Now the main application:

public App()
{
	InitializeComponent();
	var mainPage = new MainPage { BindingContext = AppContainer.Container.Resolve<IMainViewModel>() };
	MainPage = mainPage;

	Device.BeginInvokeOnMainThread
	(
		async () =>
		{
			await Task.Delay(5000);
			var secondPage = new SecondPage();
			MainPage = secondPage;
			await Task.Delay(5000);
			MainPage = mainPage;
			secondPage = null;
			GC.Collect();
		});
}

We set the main page, wait five seconds, set the second page, wait five seconds, and go back to the original main page again.  Just to make sure we have destroyed the second page, we hit it with a sledge-hammer: assign it to null and call for garbage collection. For the record, nobody ever does this!

The expected result: according to the IOC Container folks, the second view model will see that the second page is out of scope and will destroy itself.

Actual result: Nothing.  Na-da.  Zippo.  The second view model keeps on announcing that it is still alive. This goes on forever.

Image 1
Should-a, Could-a

That’s a big miss for a bunch of programmers who apparently believe they have created an object life-cycle for the variables in their global static container. But the warning sign was in front of our faces all along:

using (var scope = AppContainer.Container.BeginLifetimeScope())

That is physically impossible.  You would (minimally) have to:

  • Create a lifetime scope using “this” so the hosting class variable could be stored and monitored: 
    using (var scope = AppContainer.Container.BeginLifetimeScope(this))
  • Require that the hosting class implement an interface that can support monitoring. Unfortunately , IDisposable does not do this!  You need an interface with an event attached. So the IOC programmers will need to create one: 
    public interface IReportDisposal
    {
    	event EventHandler<object> IsDisposing;
    }
  • If the call to BeginLifetimeScope is called by any class not implementing this interface, a compile-time error must be issued!
  • The hosting class *must* report disposal accurately.  That is quite a head-ache. Remember: every class every using this sort of paradigm must implement the interface and raise the event on their own disposal.
  • The IOC Container *must* monitor the IsDisposing event, and when received, *must* destroy the instance of the view model.
  • The real reason that this is not done is that it will create a lot of work for anyone using an IOC Container. The illusion of these containers is that they are easy to use. So reality would set in and the containers would probably be abandoned.

 

Takeaways

IOC Containers are an anti-pattern because:

  1. They are not at all IOC; they are dependency injection toys;
  2. They create global variables when none are needed;
  3. They are “too accessible” – in a class C# application, privacy rules the day. We don’t want everything to have access to everything else.
  4. They issue new instances of variables that should almost always be singletons;
  5. They leverage hyper-simplistic logic in instantiating classes that does not support the level of complexity and nuance present in most C# applications;
  6. They do not handle variable life-cycle; all variables are global variables, regardless of their so-called “scope”.

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 IOCAntipattern.sln.

The code is published as open source and without encumbrance.

The post The IOC Container Anti-Pattern appeared first on Marcus Technical Services.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author


Comments and Discussions

 
GeneralI am not agree with most of statements. Pin
Member 63998218-Dec-19 7:47
MemberMember 63998218-Dec-19 7:47 
GeneralMy vote of 1 Pin
Halden4-Dec-19 4:01
MemberHalden4-Dec-19 4:01 
GeneralMy vote of 1 Pin
Member 101897433-Nov-19 21:02
MemberMember 101897433-Nov-19 21:02 
QuestionPlease check out a pull request + my intake Pin
Postnik9-Sep-18 18:54
MemberPostnik9-Sep-18 18:54 
QuestionI've long suspected this Pin
David Sherwood20-Apr-18 9:51
MemberDavid Sherwood20-Apr-18 9:51 
AnswerRe: I've long suspected this Pin
wkempf22-Apr-18 10:30
Memberwkempf22-Apr-18 10:30 
PraiseMy vote of 5! Pin
jediYL16-Apr-18 16:35
professionaljediYL16-Apr-18 16:35 
QuestionYou don't understand the topic PinPopular
wkempf16-Apr-18 5:44
Memberwkempf16-Apr-18 5:44 
AnswerRe: You don't understand the topic Pin
wmjordan18-Apr-18 0:28
professionalwmjordan18-Apr-18 0:28 
GeneralRe: You don't understand the topic Pin
wkempf18-Apr-18 3:01
Memberwkempf18-Apr-18 3:01 
GeneralRe: You don't understand the topic Pin
wmjordan18-Apr-18 16:17
professionalwmjordan18-Apr-18 16:17 
AnswerRe: You don't understand the topic Pin
marcusts20-Apr-18 10:17
Membermarcusts20-Apr-18 10:17 
GeneralRe: You don't understand the topic Pin
wkempf22-Apr-18 10:21
Memberwkempf22-Apr-18 10:21 
GeneralRe: You don't understand the topic Pin
marcusts25-Apr-18 9:05
Membermarcusts25-Apr-18 9:05 
GeneralRe: You don't understand the topic Pin
wkempf26-Apr-18 2:46
Memberwkempf26-Apr-18 2:46 
GeneralRe: You don't understand the topic Pin
marcusts26-Apr-18 17:30
Membermarcusts26-Apr-18 17:30 
GeneralRe: You don't understand the topic Pin
wkempf27-Apr-18 2:35
Memberwkempf27-Apr-18 2:35 
SuggestionRe: You don't understand the topic Pin
DevOvercome26-Apr-18 11:22
professionalDevOvercome26-Apr-18 11:22 
AnswerRe: You don't understand the topic Pin
Anders Baumann13-May-18 23:26
MemberAnders Baumann13-May-18 23:26 
GeneralRe: You don't understand the topic Pin
Member 101897433-Nov-19 21:11
MemberMember 101897433-Nov-19 21:11 
GeneralMy vote of 3 Pin
mesta16-Apr-18 3:06
Membermesta16-Apr-18 3:06 
GeneralRe: My vote of 3 Pin
marcusts20-Apr-18 10:19
Membermarcusts20-Apr-18 10:19 
GeneralCan't fully agree with you Pin
Klaus Luedenscheidt14-Apr-18 21:02
MemberKlaus Luedenscheidt14-Apr-18 21:02 
GeneralRe: Can't fully agree with you Pin
marcusts20-Apr-18 10:26
Membermarcusts20-Apr-18 10:26 
AnswerExactly! Pin
Sergey Alexandrovich Kryukov4-Mar-19 21:57
MemberSergey Alexandrovich Kryukov4-Mar-19 21:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Technical Blog
Posted 14 Apr 2018

Tagged as

Stats

12.3K views
7 bookmarked