dotnet / msbuild

The Microsoft Build Engine (MSBuild) is the build platform for .NET and Visual Studio.
https://docs.microsoft.com/visualstudio/msbuild/msbuild
MIT License
5.23k stars 1.35k forks source link

[Bug]: Transitive ProjectReference Does Not Respect Build Configuration #8580

Closed mneundorfer closed 1 year ago

mneundorfer commented 1 year ago

Issue Description

I have three projects A, B and C:

Project A:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <ProjectReference Include="..\ProjectB\ProjectB.csproj" />
    </ItemGroup>

</Project>

Project B:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <ProjectReference Include="..\ProjectC\ProjectC.csproj" />
    </ItemGroup>

</Project>

Project C:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

</Project>

In a solution, I have added Project A, and - since I have a project reference on it - Project B - but not Project C, since I don't have a direct dependency on that. When now building the solution, the following output appears:

➜  dotnet build -c Release                                                                                                                                                                                                                                                                          [0] : took 2s
Microsoft (R) Build Engine version 17.0.1+b177f8fa7 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  ProjectC -> /home/mn/code/issue/ProjectC/bin/Debug/net6.0/ProjectC.dll
  ProjectB -> /home/mn/code/issue/ProjectB/bin/Release/net6.0/ProjectB.dll
  ProjectA -> /home/mn/code/issue/ProjectA/bin/Release/net6.0/ProjectA.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

As you can see, Projects A and B respect the Release configuration of the build, while Project C does not and instead puts the generated DLL into the Debug directory. Is there something I am missing in my setup? Why does it not work as I would expect it (Project C is actually built considering the Release config)?

Steps to Reproduce

Build the solution with the Release configuration, i.e.

dotnet build -c Release

Expected Behavior

All projects are adding their output into their respective Release subdirectories

Actual Behavior

Project C has its output in the Debug subdirectory

Analysis

No response

Versions & Configurations

No response

jrdodds commented 1 year ago

Project A does not have a direct dependency but Project B does. As a work-around, add Project C to your solution.

I haven't tried to reproduce this bug but I can say that in Legacy projects a ProjectReference to a project that is not in the solution is (or was) an error.

AR-May commented 1 year ago

Team triage: MSBuild gets the projects configuration for the build from the solution file. In case it could not do that (for example, when project is not added to the solution), the project's configuration defaults to Debug. So, this is intended behavior and not a bug. The work-around described above, adding project C to the solution, is actually the real fix.

mneundorfer commented 1 year ago

Thanks for the replies!

Since the solution itself does not cause any obvious errors like described by @jrdodds, it at least feels a little bit inconsistent if on the other hand it is required to introduce a direct dependency to an otherwise transitive dependency

Then again, maybe my approach is flaky in the first place: I have a set of common libs which internally depend on each other (B->C), and want to avoid having to PackageReference all of them into my application A. Maybe there's a better approach I just don't see?

jrdodds commented 1 year ago

It's fine for A to have a ProjectReference to B, and for B to have a ProjectReference to C. The project file for A shouldn't and doesn't need to know about B's dependencies. The dependencies can change over time and will be resolved at the time of the build.

mneundorfer commented 1 year ago

Sorry, that's a typo above - of course I meant ProjectReference

But what you describe is exactly my issue: I don't want to have a ProjectReference from A to C. But this results in the behavior described in this issue, and later on makes the dotnet publish -c Release --no-build fail. Because the assemblies for Project C cannot be found where they are expected (they don't exist in bin/Release, but only in bin/Debug)

jrdodds commented 1 year ago

There is no reason for Project A to have a ProjectReference to C.

The solution file maps a 'solution level' configuration to a 'project level' configuration. The default 'Release' configuration of the solution file will indicate that Project A (Release), Project B (Release), and Project C (Release) should be used.

mneundorfer commented 1 year ago

Mhh.. Still I would argue that the first bullet point kind of violates the rule of not having to know about a projects transitive dependencies. Sure, Project A does not need to know in this case - but whoever creates the solution must know about it.

Anyways, I can live with the provided workaround/fix. Thanks for the support!

ADD-David-Antolin commented 1 year ago

I think that the ProjectReference should inherit the Configuration value of the Project that references it. In fact, that is what happens if instead of compiling the solution file it compiles 'ProjectA' directly.

It seems a great incoherence that solution and project builds behave so differently.

Please, reconsider reopening this issue and fixing it.

jachstet-sea commented 1 year ago

Hi, in our solution(s) we are experiencing the same. It gets even more confusing if "Project C" has a conditional dependency that are only used in case of a specific build configurations. "dotnet restore" doesn't know about "Build Configurations", it fails when the -c switch is used. So I run a "dotnet restore" on the solution and it restores some packages but not necessarily those that are needed when I later run the "dotnet build" WITH specifying the build configuration. Thus, the build fails since assemblies are missing. I would highly advocate passing down the specified build configuration to transitive ProjectReferences.

I also agree that not all projects only used as indirect dependencies should be part of the solution file. Only direct dependencies should need to be specified. This would also match Visual Studio behaviours when convertig NuGet references from the packages.config format to PackageReferences in the .csproj files. It advises you to only specify direct dependencies and drops all those which are only indirect. Some consistency would be nice!

Speaking of PackageReferences: That's also a possible workaround to using ProjectReferences. It just makes chaning stuff over lots of different projects and debugging it harder.