For quite some years now, we (the NDepend team) got some demand about resolving Dependency Injection, see this page on our User Voices. Lately we’ve been considering such support carefully but came to the conclusion that it’d be awkward. On the other hand we have alway been prioritizing features development based on user feedback and need. So our conclusions are not definitive yet. I write this post to not only expose our point of view, but eventually trigger more productive discussions.
Static Analysis
First let’s underline that NDepend is a static analyzer. It gathers 100% of its data from the code itself, and also parses more data like code coverage. Static analysis is in opposition with dynamic analysis that gathers data from runtime execution.
- Static analysis deals with classes, methods and design measures like lines of code or complexity. It is design-time analysis.
- Dynamic analysis deals with objects and runtime measures like performance or memory consumption. It is run-time analysis
Dependency Injection (DI)
Recently I explained the basis of Dependency Injection (DI) and how it relates to DIP in the post SOLID Design: The Dependency Inversion Principle (DIP). DI is all about depending on interfaces instead of depending on classes. Here is simple DI sample code, ClientCode() depends on IDbConnection and not on SqlConnection. The power of DI is that now ClientCode() can run on other RDMS than SQL Server, like Oracle or SQLLite. ClientCode() can now also be unit-tested seamlessly by mocking the interface IDbConnection.
1 2 3 4 5 6 7 |
static void DICode() { IDbConnection dbConnection = new SqlConnection(); ClientCode(dbConnection); } static void ClientCode(IDbConnection dbConnection) { // Client code has no idea what's behind IDbConnection dbConnection.Open(); } |
Stable Dependency Principle (SDP)
DI is an application of the Stable Dependency Principle (SDP), which is not part of the SOLID principles, but is much related with the Dependency Inversion Principle (DIP).
SDP states: The dependency should be in the direction of the stability. Stability is a measure of the likeliness of change. The more likely it will change, the less it is stable.
The Liskov Substitution Principle (LSP) and the Interface Segregation Principle (ISP) articles explain how interfaces must be carefully thought out. As a result an interface is a stable code element, that is less likely to change than the the classes that implement it. Following the SDP, it is better design to rely on interfaces than to rely on concrete classes.
The Users need: supporting DI through Static Analysis
In this context, users want NDepend to be able to parse DI code like that.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Autofac DI var builder = new ContainerBuilder(); builder.RegisterInstance(new SqlConnection()).As<IDbConnection>(); // UnityContainer DI IUnityContainer container = new UnityContainer(); container.RegisterType<IDbConnection, SqlConnection>(); // NInject DI public class Bindings : NinjectModule { public override void Load() { Bind<IDbConnection>().To<SqlConnection>(); } } |
Concretely users want NDepend to bind all calls to an IDbConnection member with a call to the corresponding SqlConnection member . For example a dependency toward SqlConnection.Open() should be created for any method calling IDbConnection.Open().
But why would this be useful? Here is a user feedback:
We have layered architecture: Foundation, Feature, Project. Following current NDepend report, everything follows right dependencies. Project depends on Feature and Foundation. Feature depends on Foundation. However due to some of DI registrations, we have complex dependencies (…) there is dependency from Foundation to Project in reality. Interface is declared in Foundation, but implementation is in Project and registered via DI. It means that Foundation projects, that are core and stable part of solution, depend on Project level, which is not so stable and reliable.
In reality is highlighted in bold, because the user really meant at run-time. At run-time Foundation depends on Project, the same way that at run-time ClientCode() depends on SqlConnection. But at design-time Foundation doesn’t depend on Project, the same way that at design-time ClientCode() doesn’t depends on SqlConnection. At design-time ClientCode() only knows about IDbConnection.
From this, one implies that not only at design time, but also at runtime dependencies should go from Project towards Foundation. In other words, one feels that implementing an adapter into Project and injecting it into a Foundation component “is not so stable and reliable”, but that’s not how those principles are meant and described.
As a matter of fact, being able to inject Project adapters into Foundation components (where the Project adapter implements one of Foundation’s interfaces) is the core of the Dependency Inversion Principle (DIP). Without it, it is impossible to create flexible systems. This can be unreliable, but only if one doesn’t follow the Liskov Substitution Principle (LSP), when Foundation interfaces are not well thought-out, like for example when ICollection<T> forces the Array class to implement ICollection<T>.Add() which obviously cannot be supported.
Conclusion
Hence it looks like this feature need results from a confusion between design-time dependencies and run-time dependencies. The purpose of design is to reduce the cost of change, anything else is not design. Managing design-time dependencies is at the heart of design. All SOLID principles and all principles surrounding SOLID like the Stable Dependency Principle (SDP) are about design-time dependencies.
Moreover adding DI support would be a never-ending story—and I guess it cannot be done right without information collected at run-time. DI Containers are very complex beasts internally, their behavior often changes from one major version to the next, and there are dozens of DI Containers in .NET (Autofac, UnityContainer, NInject, StructureMap, MEF, SimpleInjector, Castle.Windsor, LightInject, Prism, Spring.net … just to name a few).
Again all our developments are driven by users-need, but on this particular need we’re not sure it’d make sense. We let the discussion open on this post and on the User Voices page. Whether you agree or disagree with our positions, feel free to comment.
Hey, what about Windsor 😉
jk. This looks like a really useful area to explore.
Sure Windsor as well 🙂
>This looks like a really useful area to explore.
You mean that NDepend should try to guess what’s behind a DI interface?
Yes, that and more broadly providing more analysis and metrics around loosely composed applications.
I wouldn’t go for guessing though, like you pointed out, there’s way too many moving parts. There’d have to be some intermediate/universal way to snapshot the container (perhaps as a part of a test) and feed that to NDepend for analysis
Hi Patrick, I think this post is spot on. NDepend should not try to figure out run-time dependencies. In a layered architecture, lower layers often contain interfaces allowing them to hook into behavior from higher layers at runtime. This should absolutely not change the dependency graph or dependency matrix.
You are exactly right. In my opinion, trying to add DI support would be a big waste of your time. I have been a customer for several years and would never use such a feature.
Krzysztof William, thanks for your opposite answers 🙂
Krzysztof, indeed the only proper way to do that would be by importing dynamic analysis result in the NDepend code model. We haven’t introduce such import so far but there would be many area that could benefit from dynamic data certainly.
To clarify, has NDepend decided not to support dependency injection (DI) because:
1. Doing so would be difficult & time-consuming?
2. DI is effectively a run-time behavior/concept, and NDepend is about static code analysis?
I completely understand the former, but I can’t say that I agree with the later.
@Pressacco
We could have 2 modes, static and dynamic analysis modes, that would offer very different dependency graphs between classes and methods. But we have a huge roadmap to improve even more our static analysis features: Better be excellent at what one does than doing a bit of everything.