Click here to Skip to main content
15,888,286 members
Articles / All Topics
Technical Blog

The IOC Container Anti-Pattern

Rate me:
Please Sign up or sign in to vote.
4.42/5 (17 votes)
14 Apr 2018CPOL10 min read 26.8K   7   27
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.

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)



Comments and Discussions

 
GeneralI am not agree with most of statements. Pin
Member 63998218-Dec-19 7:47
Member 63998218-Dec-19 7:47 
GeneralMy vote of 1 Pin
Halden4-Dec-19 4:01
Halden4-Dec-19 4:01 
GeneralMy vote of 1 Pin
Member 101897433-Nov-19 21:02
Member 101897433-Nov-19 21:02 
QuestionPlease check out a pull request + my intake Pin
Postnik9-Sep-18 18:54
Postnik9-Sep-18 18:54 
QuestionI've long suspected this Pin
David Sherwood20-Apr-18 9:51
David Sherwood20-Apr-18 9:51 
AnswerRe: I've long suspected this Pin
wkempf22-Apr-18 10:30
wkempf22-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
wkempf16-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
wkempf18-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
marcusts20-Apr-18 10:17 
GeneralRe: You don't understand the topic Pin
wkempf22-Apr-18 10:21
wkempf22-Apr-18 10:21 
GeneralRe: You don't understand the topic Pin
marcusts25-Apr-18 9:05
marcusts25-Apr-18 9:05 
GeneralRe: You don't understand the topic Pin
wkempf26-Apr-18 2:46
wkempf26-Apr-18 2:46 
GeneralRe: You don't understand the topic Pin
marcusts26-Apr-18 17:30
marcusts26-Apr-18 17:30 
GeneralRe: You don't understand the topic Pin
wkempf27-Apr-18 2:35
wkempf27-Apr-18 2:35 
marcusts wrote:
In order to prove that you are right and that I am wrong, you must produce an example of how Autofac and regular users consume a DI container and demonstrate that it destroys variables in a timely way, as you have claimed.


I did (with regard to lifetime, not usage). You're just not understanding what "destroys" (documentation says disposes) means. You're expecting/wanting behavior that's impossible.

Since usage is obviously something you're struggling with, I'd suggest looking at any ASP.NET Core project to see how containers are properly used. Note that in this usage the application code doesn't have access to the container and never once calls Resolve. This is proper usage of a DI container.

marcusts wrote:
All your current codes does is to Resolve and then immediately nullify variables
.

Take that line of code out. You won't see a difference in behavior.

marcusts wrote:
Everyone knows that this will cause their destruction 100% of the time.


Then what everyone knows is wrong. This does not cause destruction at all. It allows the GC to collect the object, if and only if there are no other references. Keep that last bit in mind, as it's super important to what I say next.

marcusts wrote:
t does not prove anything.


In my code, it proves the container maintains references only to IDisposable types, allowing all other objects to be collected following the normal rules, i.e. the GC can collect the object once there are no other references. In both of your tests you maintain references to the object, so the GC cannot collect it. In your final test you have a comment that indicates you think the DI container is somehow supposed to remove this reference magically, which it cannot do, nor should it if it could. Please read that last sentence over and over until it sinks in. This is the heart of your misunderstanding.

marcusts wrote:
* That it globalizes access to variables in a way that violates C# fundamentals. Any variable can be accessed by anyone at any time. C# advises that variables be as private as possible to increase safety while reducing hidden dependencies.


No, it doesn't globalize access. You did by using the service locator pattern rather than dependency injection. BTW, C# is a programming language and cannot "advise" anything. If it could, this wouldn't be it's advise, since it has features specific to doing the opposite of this (static members).

marcusts wrote:
* That it encourages sloppy assignments that are not inherently type-safe. This does not mean it causes them. It just makes it way to easy to make mistakes. Programmers are already prone to these sorts of errors. A type-safe approach would (minimally) insist that interfaces and types are compatible when declared.


No, it does not. Autofac, not "DI containers", allows, not encourages.

marcusts wrote:
* That it casually issues new copies of global variables which then create a confusing "state" where no variable is in authority about the data that the variable is managing. This can be defeated by creating Singletons or "PerDependency" registrations. I recommend that. I still don't like the global variables, so this really is the lesser of two evils.


There are no "global variables". If you use service locator you have exposed a global variable, which in turn exposes global state (not global variables). And, again, everything that is wrong with global variables/state is true for singletons. You both complain there's global state as well as complain that there isn't all in the same breath here. Use a DI container properly and none of your complaints here survive.

marcusts wrote:
* That it initializes variables without intelligence. Our ability to create classes through injection is one of our key forms of control. As a programmer, I don't want to give that up. This limitation means that DI Containers create extremely simplistic apps that (ironically) lack flexible, variable injection. They are a static, single-injection strategy, which is a weak approach.


The reality is that 80% (made up statistic) of the time, you want a single type constructed the same way. DI containers optimize for this. The other 20% (still made up) of the time are still options with a DI container, using factories that give consuming code complete control.

BTW, there's a huge army of ASP.NET developers ready to take extreme issue with you telling them they create "simplistic apps". I've written many very complex apps, ranging from web services to data replication engines, that have used, even depended upon, DI containers, so I take issue here myself.

marcusts wrote:
If we cannot agree on the lifecycle management issue, then certainly we can agree on the rest.


Which is sad. Look, in this very article you said that your position would be controversial. So at some level you understand most people would not agree with the very premise, much less the "facts" you use to defend the premise. So, either most people are idiot "sheeple" following trends, or there's something you're not understanding. I'm "attacking" you (your words, not my intent) because I point out that, yes, indeed, you don't understand something, and rather than making an effort to understand why I said that all that you've done is repeat the same claims over and over. Try to take the emotion out of it for a second and *think* about the things I've said. Spend a few hours reading about service locator and dependency injection, including the many links people have sent you on this topic, including the ones that talk about service locator being an anti-pattern. Realize that more than half of your complaints here are the very reasons service locator is considered an anti-pattern. Then realize these articles are telling you that DI is how you solve the problems of service locator. Follow that thread of logic to it's conclusion and realize that I was not attempting to "tear you down" but giving you advise when I said you don't understand... because you don't. Once you do understand, you can come back and tell me you still think DI containers are an anti-pattern, and I'll have no issue with that. Heck, you may even be able to convince me to agree with you... our field has a long history of insights like this changing the way we view things. Once upon a time there were great debates amongst the experts about a radical proclamation that "goto" is harmful. But, until you can talk about this subject from a firm understanding of it, you won't succeed there.

* Don't conflate an implementation with the pattern.
* Don't use service locator to prove anything about dependency injection.
* Understand how the GC works and what a DI container provides you with regards to lifetime.
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
Anders Baumann13-May-18 23:26 
GeneralRe: You don't understand the topic Pin
Member 101897433-Nov-19 21:11
Member 101897433-Nov-19 21:11 
GeneralMy vote of 3 Pin
mesta16-Apr-18 3:06
mesta16-Apr-18 3:06 
GeneralRe: My vote of 3 Pin
marcusts20-Apr-18 10:19
marcusts20-Apr-18 10:19 
GeneralCan't fully agree with you Pin
Klaus Luedenscheidt14-Apr-18 21:02
Klaus Luedenscheidt14-Apr-18 21:02 
GeneralRe: Can't fully agree with you Pin
marcusts20-Apr-18 10:26
marcusts20-Apr-18 10:26 
AnswerExactly! Pin
Sergey Alexandrovich Kryukov4-Mar-19 21:57
mvaSergey 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.