2

We have an issue with the deterministic build in .NET8.

We have a large project with many referenced libraries.

For simplification, lets have lib2 which is referenced by lib1.

If source code of lib2 changes, then projects are build, we need lib1 dll to remain unchanged.

The current behavior in .NET8 is that if source code of lib2 changes, msbuild will produce different dll (which is correct) but it will change lib1 dll as well.

For deployment build we use msbuild.

Is this a bug or an intended solution? Is there a solution to our requirements?

3
  • 4
    Are your libraries versioned? If so, that sounds entirely by design - if you previously built lib2 against lib1 version 1.2.0, then rebuilt it against lib1 version 1.3.0, I'd expect that version change to be reflected in the manifest. Commented Sep 27, 2024 at 11:53
  • No, they are not. Commented Sep 27, 2024 at 12:05
  • Is incremental build for lib1 broken, i.e. does lib1 build when everything is up to date? There are a couple of common causes for a broken incremental build. (e.g. If a file in lib1 has "Copy to Output Directory" set to "Copy always", the project will always build to perform the copy. To allow for an incremental build, use "Copy if newer".) Commented Sep 27, 2024 at 14:30

1 Answer 1

1

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:

  1. Project A is dependent on Project B
  2. Project B's internal code changes
  3. Project B is built
  4. 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:

  1. Project A is dependent on Project B
  2. Project B's code changes, affecting its public interface
  3. Project B is built
  4. 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:

  1. A package management system like NuGet or Chocolatey.
  2. Implementation and observance of a semantic versioning strategy.
  3. 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

Sign up to request clarification or add additional context in comments.

3 Comments

But according to our observation, even in Scenario 1, lib A will change if a large amount of internal code is added to B.
Just to check when you say internal code, you mean code that is not visible to the outside world, that A cannot see? If this is the case I am mildly surprised. That said, there's no hard "contract" that says that project A should never produce a new binary when nothing has changed; it's really observed behaviour based on the determinism of the inputs. If you really wanted to find out what's happening, see if they are binary same. Do a binary comparison, or even better load them up in IlSpy.
But I have to underline, that if your pipeline is pinned on the gospel that project A's binary shalt not change - just stop this pain and start using a proper packaging system. .NET just isn't meant to manage dependency resolution with this level of competence you're expecting of it.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.