I'll go through your points numerically, but first, there's something you should be very careful of: don't conflate how a consumer uses a library with how the library is implemented. Good examples of this are Entity Framework (which you yourself cite as a good library) and ASP.NET's MVC. Both of these do an awful lot under the hood with, for example, reflection, which would absolutely not be considered good design if you spread it through day-to-day code.
These libraries are absolutely not "transparent" in how they work, or what they're doing behind-the-scenes. But that's not a detriment, because they support good programming principles in their consumers. So whenever you talk about a library like this, remember that as a consumer of a library, it's not your job to worry about its implementation or maintenance. You should only worry about how it helps or hinders the code you write which uses the library. Don't conflate these concepts!
So, to go through by point:
Immediately we reach what I can only assume is an example of the above. You say that IOC containers set up most dependencies as static. Well, perhaps some implementation detail of how they work includes static storage (though given that they tend to have an instance of an object like Ninject's IKernel
as their core storage, I doubt even that). But this is not your concern!
Actually, an IoC container is just as explicit with scope as poor man's dependency injection. (I'm going to keep comparing IoC containers to poor man's DI because comparing them to no DI at all would be unfair and confusing. And, to be clear, I'm not using "poor man's DI" as a pejorative.)
In poor man's DI, you'd instantiate a dependency manually, then inject it into the class that needs it. At the point where you construct the dependency, you'd choose what to do with it- store it in a locally scoped variable, a class variable, a static variable, don't store it at all, whatever. You could pass the same instance into lots of classes, or you could create a new one for each class. Whatever. The point is, to see which is happening, you look to the point- probably near the application root- where the dependency is created.
Now what about an IoC container? Well, you do exactly the same! Again, going by Ninject terminology, you look at where the binding is set up, and find whether it says something like InTransientScope
, InSingletonScope
, or whatever. If anything this is potentially clearer, because you have code right there declaring its scope, rather than having to look through a method to track what happens to some object (an object may be scoped to a block, but is it used multiple times in that block or just once, for example). So maybe you're repelled by the idea of having to use a feature on the IoC container rather than a raw language feature to dictate scope, but as long as you trust your IoC library- which you should!- there's no real disadvantage to it.
I still don't really know what you're talking about here. Do IoC containers look at private properties as part of their internal implementation? I don't know why they would, but again, if they do, it's not your concern how a library you're using is implemented.
Or, maybe, they expose a capability like injecting into private setters? Honestly I've never encountered this, and I'm dubious about whether this really is a common feature. But even if it's there, it's a simple case of a tool that can be misused. Remember, even without an IoC container, it's but a few lines of Reflection code to access and modify a private property. It's something you should almost never do, but that doesn't mean .NET is bad for exposing the capability. If somebody so obviously and wildly misuses a tool, it's the person's fault, not the tool's.
The ultimate point here is similar to 2. The vast majority of the time, IoC containers DO use the constructor! Setter injection is offered for very specific circumstances where, for particular reasons, constructor injection can't be used. Anybody who uses setter injection all the time to cover up how many dependencies are being passed in is massively absuing the tool. That's NOT the tool's fault, it's theirs.
Now, if this was a really easy mistake to innocently make, and one that IoC containers encourage, then okay, maybe you'd have a point. It'd be like making every member of my class public then blaming other people when they modify things they shouldn't, right? But anybody who uses setter injection to cover up violations of SRP is either willfully ignoring or completely ignorant of basic design principles. It's unreasonable to lay the blame for this at the IoC container.
That's especially true because it's also something you can do just as easily with poor man's DI:
var myObject
= new MyTerriblyLargeObject { DependencyA = new Thing(), DependencyB = new Widget(), Dependency C = new Repository(), ... };
So really, this worry seems completely orthogonal to whether or not an IoC container is used.
Changing how your classes work together is not a violation of OCP. If it was, then all dependency inversion would be encouraging a violation of OCP. If this was the case, they wouldn't both be in the same SOLID acronym!
Furthermore, neither points a) nor b) come close to having anything to do with OCP. I don't even know how to answer these with respect to OCP.
The only thing I can guess is that you think OCP is something to do with behaviour not being altered at runtime, or about where in the code the lifecycle of dependencies is controlled from. It's not. OCP is about not having to modify existing code when requirements are added or change. It's all about writing code, not about how code you've already written is glued together (though, of course, loose coupling is an important part of achieving OCP).
And one final thing you say:
But I can’t put that same trust in a third party tool that is altering my compiled code to perform workarounds to things I wouldn’t be able to do manually.
Yes, you can. There is absolutely no reason for you to think that these tools- relied on by vast numbers of projects- are any more buggy or prone to unexpected behaviour than any other third-party library.
Addendum
I just noticed your intro paragraphs could use some addressing too. You sardonically say that IoC containers "aren’t employing some secret technique we’ve never heard of" to avoid messy, duplication-prone code to build a dependency graph. And you're quite right, what they're doing is actually addressing these things with the same basic techniques as we programmers always do.
Let me talk you through a hypothetical scenario. You, as a programmer, put together a large application, and at the entry point, where you're constructing your object graph, you notice you have quite messy code. There are quite a few classes that are used again and again, and every time you build one of those you have to build the whole chain of dependencies under them again. Plus you find you don't have any expressive way of declaring or controlling the lifecycle of dependencies, except with custom code for each one. Your code is unstructured and full of repetition. This is the messiness you talk about in your intro paragraph.
So first, you start to refactor a bit- where some repeated code is structured enough you pull it out into helper methods, and so on. But then you start to think- is this a problem that I could perhaps tackle in a general sense, one that isn't specific to this particular project but could help you in all your future projects?
So you sit down, and think about it, and decide that there should be a class that can resolve dependencies. And you sketch out what public methods it would need:
void Bind(Type interfaceType, Type concreteType, bool singleton);
T Resolve<T>();
Bind
says "where you see a constructor argument of type interfaceType
, pass in an instance of concreteType
". The additional singleton
parameter says whether to use the same instance of concreteType
each time, or always make a new one.
Resolve
will simply try to construct T
with any constructor it can find whose arguments are all of types which have previously been bound. It can also call itself recursively to resolve the dependencies all the way down. If it can't resolve an instance because not everything has been bound, it throws an exception.
You can try implementing this yourself, and you'll find you need a bit of reflection, and some caching for the bindings where singleton
is true, but certainly nothing drastic or horrifying. And once you're done- voila, you have the core of your very own IoC container! Is it really that scary? The only real difference between this and Ninject or StructureMap or Castle Windsor or whatever one you prefer is that those have a lot more functionality to cover the (many!) use cases where this basic version wouldn't be sufficient. But at its heart, what you have there is the essence of an IoC container.
IOC
andExplicitness of code
is exactly the thing I have problems with. Manual DI can easily become a chore, but it at least puts selfcontained explicit program flow in one place - "You get what you see, you see what you get". While IOC containers' bindings and declarations can easily become a hidden parallel program on its own.