nugetnuget-packagevisual-studio-2022transitive-dependencypackagereference

Visual Studio 2022 not adding transitive dependency after migrating from package.config to PackageReference


I'm running into this issue after upgrading to VS2022 from VS2019. As part of the upgrade I migrated from NuGet package.config to PackageReference

I have VS 2022 17.7.5, .NET Framework 4.8 Project A which has these top level nuget packages (using nuget PackageReference)

  1. NewtonSoft.Json
  2. MailKit
  3. HtmlAgilityPack
  4. INIFileParserDotNetCore.Signed

As part of these packages nuget all identified a whole bunch of Transitive packages including BouncyCastle.Cryptography, MimeKit, System.Buffers and a bunch of System.xxx packages including on package called System.Runtime.CompilerServices.Unsafe.

Then I have Project B (also .NET Framework 4.8) which has a dependency on Project A (above) (there are no other direct dependencies or nuget packages in Project B)

My understanding is that I don't need to install transitive packages and that the system takes care of it automatically while building.

When I build Project A, all the transitive dependencies show in the bin folder.

However when I build Project B, all the transitive dependencies except for System.Runtime.CompilerServices.Unsafe are showing in the bin folder. That's causing a lot of grief because when Project B calls a method in Project A, it requires System.Runtime.CompilerServices.Unsafe to operate and instead now throws an exception that it can't load the System.Runtime.CompilerServices.Unsafe dll. Just to be clear there's no error at build time, at runtime when it calls a method that's when the error occurs because the dll isn't copied by Visual Studio to the bin directory, nor do I see the transient dependencies in the References of the project.

Why is that just that one System.Runtime.CompilerServices.Unsafe transitive dependency is not working it way into Project B? It was working perfectly in VS2019 but after upgrading to VS2022 it's stopped working. If I manually add the transitive dependencies to project B, it then gets copied to the build directory, but I shouldn't have to do that manually right?. Is something about VS2022 different from VS2019. What's the correct way to fix it?

PS - I've tried to remove ALL nuget packages and then reinstall them, that didn't work (actually at first something was wrong with Nuget, it never created references to installed packages, but after a clean reinstall it then finally created the references to the installed packages, but that one transitive packages still don't show in Project B after building it or in the dependencies)

Here the complete .csproj file for Project B. Project B files are referenced as XXXXXXX and Project A is referenced directly.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
  <PropertyGroup>
    <ProjectType>Local</ProjectType>
    <ProductVersion>8.0.30319</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{6DC6774A-0161-45A0-9392-E19570BB30B7}</ProjectGuid>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ApplicationIcon>Resources\XXXXXXX_Icon.ico</ApplicationIcon>
    <AssemblyName>XXXXXXX</AssemblyName>
    <AssemblyOriginatorKeyFile>XXXXXXX.snk</AssemblyOriginatorKeyFile>
    <DelaySign>false</DelaySign>
    <OutputType>WinExe</OutputType>
    <RootNamespace>XXXXXXX</RootNamespace>
    <StartupObject>XXXXX.YYYYY</StartupObject>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
    <TargetFrameworkProfile>
    </TargetFrameworkProfile>
    <FileAlignment>512</FileAlignment>
    <IsWebBootstrapper>false</IsWebBootstrapper>
    <SccProjectName>SAK</SccProjectName>
    <SccLocalPath>SAK</SccLocalPath>
    <SccAuxPath>SAK</SccAuxPath>
    <SccProvider>SAK</SccProvider>
    <SignAssembly>true</SignAssembly>
    <PublishUrl>publish\</PublishUrl>
    <Install>true</Install>
    <InstallFrom>Disk</InstallFrom>
    <UpdateEnabled>false</UpdateEnabled>
    <UpdateMode>Foreground</UpdateMode>
    <UpdateInterval>7</UpdateInterval>
    <UpdateIntervalUnits>Days</UpdateIntervalUnits>
    <UpdatePeriodically>false</UpdatePeriodically>
    <UpdateRequired>false</UpdateRequired>
    <MapFileExtensions>true</MapFileExtensions>
    <ApplicationRevision>0</ApplicationRevision>
    <ApplicationVersion>1.0.0.%2a</ApplicationVersion>
    <UseApplicationTrust>false</UseApplicationTrust>
    <BootstrapperEnabled>true</BootstrapperEnabled>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <OutputPath>bin\Debug\</OutputPath>
    <AllowUnsafeBlocks>false</AllowUnsafeBlocks>
    <BaseAddress>285212672</BaseAddress>
    <CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
    <ConfigurationOverrideFile />
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DocumentationFile />
    <DebugSymbols>true</DebugSymbols>
    <FileAlignment>512</FileAlignment>
    <Optimize>false</Optimize>
    <RegisterForComInterop>false</RegisterForComInterop>
    <RemoveIntegerChecks>false</RemoveIntegerChecks>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
    <WarningLevel>4</WarningLevel>
    <DebugType>full</DebugType>
    <ErrorReport>prompt</ErrorReport>
    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
    <UseVSHostingProcess>true</UseVSHostingProcess>
    <Prefer32Bit>false</Prefer32Bit>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <OutputPath>bin\Release\</OutputPath>
    <AllowUnsafeBlocks>false</AllowUnsafeBlocks>
    <BaseAddress>285212672</BaseAddress>
    <CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
    <ConfigurationOverrideFile />
    <DefineConstants>TRACE</DefineConstants>
    <DocumentationFile />
    <DebugSymbols>false</DebugSymbols>
    <FileAlignment>512</FileAlignment>
    <Optimize>true</Optimize>
    <RegisterForComInterop>false</RegisterForComInterop>
    <RemoveIntegerChecks>false</RemoveIntegerChecks>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
    <WarningLevel>4</WarningLevel>
    <DebugType>none</DebugType>
    <ErrorReport>prompt</ErrorReport>
    <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
    <Prefer32Bit>false</Prefer32Bit>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Drawing" />
    <Reference Include="System.Security" />
    <Reference Include="System.Windows.Forms" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="XXXXXXX.cs">
      <SubType>Form</SubType>
    </Compile>
    <Compile Include="Properties\Resources.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <EmbeddedResource Include="XXXXXXX.resx">
      <DependentUpon>XXXXXXX.cs</DependentUpon>
    </EmbeddedResource>
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="app.config" />
    <None Include="XXXXXXX.snk" />
  </ItemGroup>
  <ItemGroup>
    <BootstrapperPackage Include=".NETFramework,Version=v4.8">
      <Visible>False</Visible>
      <ProductName>Microsoft .NET Framework 4.8 %28x86 and x64%29</ProductName>
      <Install>true</Install>
    </BootstrapperPackage>
    <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
      <Visible>False</Visible>
      <ProductName>.NET Framework 3.5 SP1</ProductName>
      <Install>false</Install>
    </BootstrapperPackage>
  </ItemGroup>
  <ItemGroup>
    <Content Include="Resources\XXXXXXX_Icon.ico" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\ProjectA\CommonForms.csproj">
      <Project>{e0c409ed-c95f-473b-9f0c-d17abf252934}</Project>
      <Name>CommonForms</Name>
      <EmbedInteropTypes>False</EmbedInteropTypes>
    </ProjectReference>
    <ProjectReference Include="..\ProjectA\CommonUtils.csproj">
      <Project>{0c9d9170-37e6-4430-ae17-79ad863e4211}</Project>
      <Name>CommonUtils</Name>
      <EmbedInteropTypes>False</EmbedInteropTypes>
    </ProjectReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <PropertyGroup>
    <PostBuildEvent>xcopy "$(ProjectDir)..\Other Stuff\*.exe" $(TargetDir) /Y
xcopy "$(ProjectDir)..\Other Stuff\*.bat" $(TargetDir) /Y</PostBuildEvent>
  </PropertyGroup>
</Project>

Solution

  • If you want to truly, deeply understand .NET builds, the one and only tool you need to learn is "binlogs" (binary logs). You can capture one on the command line by appending -bl to anything that fundamentally runs on MSBuild, so dotnet build -bl or msbuild -bl. This will create a file named msbuild.binlog in the current directory, and you can open this with https://msbuildlog.com, where there's a Windows app to download, or can be viewed directly in the browser. That will tell you, in detail, about everything that MSBuild did. It still takes a experience to understand how to interpret it, but this is generally the only tool that MSBuild experts use to investigate build issues.

    Since you didn't provide a minimal, reproducible example, I can only provide generic advice, I can't answer why in your specific example it's not working as you expect. However, given you mentioned that the project with the problem doesn't use any NuGet packages itself, I have high confidence that I know what the root cause is.

    Basically, when you build a project, the .NET build system will only copy ProjectReference dlls into the current project's bin directory. Everything else comes though a build task named ResolveAssemblyReferences, which works by analyzing dlls it already knows about, looking at assembly metadata to see what assemblies they reference, then trying to find those new assemblies/dlls on disk somewhere. When a dll is loaded by reflection, so there is no compile-time metadata in the assembly metadata (also viewable by ILSpy), then the .NET Build system has no way of knowing this assembly/dll is needed. This is also true for native assemblies that are P/Invoked by managed assemblies. Back in this world, there's no alternative to modifying your project somehow tell the build system to copy the files you need into the bin directory.

    When you install a NuGet package using packages.config, NuGet modifies the project file to tell the .NET build system that the current project needs files from the package. However, when you have a ProjectReference to a project that uses packages via packages.config, then it's exactly what I described above. NuGet's packages.config is entirely independent of the .NET build system and could have been implemented by anyone in the world. In fact, before Visual Studio 2017, NuGet was distributed as a Visual Studio extension in the VS Marketplace, just like any other extension that anyone in the world could implement. NuGet has zero integration in the overall build system.

    Starting with PackageReference, introduced around 2016, NuGet brought in the concept of transitive closures, so when you have a ProjectReference, NuGet will treat the project as a package, check the PackageReferences of the referenced project, and treat them as package dependencies. Therefore, if you convert your project using packages.confg to use PackageReference instead, and tell NuGet that your project without any packages should be treated as a PackageReference project (using the MSBuild property <RestoreProjectStyle>PackageReference</RestoreProjectStyle>), then NuGet will detect all the packages that your project references use, and tell the .NET build system that the current project should also use those packages.

    I really need to figure out a clearer way to explain this and write it up a blog post than I can point people to, rather than writing these long answers that few people read ☹️