Is this a bug or an intended solution?
It's both intended and required. You've correctly observed the dependency-triggered build of your consuming project. But it's not quite as simple as what you've described, as I'll describe below.
How .NET consumes dependencies
.NET uses dynamic link libraries (DLLs) for dependent projects. A dynamic link is placed from the consumer to the dependency. This means that the consuming project is agnostic to internal changes in the dependency, but is affected by changes in the public interface of the dependency.
Scenario 1: the internal interface of dependency DLL changes
Let us consider the scenario:
- Project A is dependent on Project B
- Project B's internal code changes
- Project B is built
- Project A is built (possibly part of a solution build incorporating criterion 3)
In this case†, Project A will not produce a new binary.
This is because Project A consumes the interface of Project B via a Dynamic Link. It is in effect uninterested in how Project B is implemented, it simply cares about the interface into which it calls.
Scenario 2: The public interface of the dependency DLL changes
Now what you might have observed if your consuming project is producing a new binary, then your dependency library's public interface has changed.
Consider the scenario:
- Project A is dependent on Project B
- Project B's code changes, affecting its public interface
- Project B is built
- Project A is built
In this case Project A needs to be re-linked in order to consume the new interface. Without a more advanced system of dependency resolution, Project A has no way to appraise whether a public interface change affects it. Project A assumes the worst case scenario and builds against the new dependency library interface.
Is there a solution to our requirements?
Of course. You're not the first team to have more advanced requirements for dependency resolution. Often teams find that leaving .NET to manage implicit dependency resolution works fine on a small scale, only to face challenges such as yours when it comes to scaling up.
If you have tight constraints over the dependency-triggering of your consuming project, you'd most likely benefit from:
- A package management system like NuGet or Chocolatey.
- Implementation and observance of a semantic versioning strategy.
- Just to cover all bases, ensuring that your packages are built with a continuous integration (CI) system with a source control management (SCM) system.
This will provide the following advantages:
- 1 will provide advanced dependency resolution, allowing consuming projects to decide whether they want to update or continue to use the same version, requiring no rebuild. It loosens the coupling of the dependency.
- 2 will provide advanced version referencing within the consuming project. For instance, if the public interface has changed, but the semantic version indicates that it's safe to "roll forward", then the package resolution system can choose to use a newer version of your dependency project when no consuming project binary has been rebuilt.
So in this alternate setup:
- you make changes to your dependency library and your consuming library would never be forced to rebuild.
- If the changes are determined compatible based on your semantic version, the consuming library can†† automatically pick up the new changes.
- If the consumer is required to make changes to use the new version (such as in a major version change), then: a) a manual update of the dependency in the consuming package is required, b) code changed in the consuming project c) a build performed. This is inescapable; however these changes were deferred due to the package versioning. It's not an automatic upgrade.
† Note a lot of caveats to this sweeping statement. For the sake of simplicity, I'm glossing over things like indeterminism, automatic versioning or anything like that. We're talking about a .NET project with everything set to default.
†† I say "can" with a lot of care here. It depends on the way that the dependency resolution is performed, the version number constraints applied on the dependency, and even the presence of other dependencies in the solution. It's a very complex subject that's beyond the scope of an SO answer. However more information is provided here: https://learn.microsoft.com/en-us/nuget/concepts/dependency-resolution