<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: S8ui</title>
    <description>The latest articles on DEV Community by S8ui (@sui23e879d).</description>
    <link>https://dev.to/sui23e879d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081585%2Fbd1d2435-529a-4ad1-b2fa-4cf37fae7717.png</url>
      <title>DEV Community: S8ui</title>
      <link>https://dev.to/sui23e879d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sui23e879d"/>
    <language>en</language>
    <item>
      <title>From HintPath to PackageReference: How One Deleted packages.config Took Down My CI Pipeline</title>
      <dc:creator>S8ui</dc:creator>
      <pubDate>Mon, 24 Aug 2026 11:54:00 +0000</pubDate>
      <link>https://dev.to/sui23e879d/from-hintpath-to-packagereference-how-one-deleted-packagesconfig-took-down-my-ci-pipeline-4bm6</link>
      <guid>https://dev.to/sui23e879d/from-hintpath-to-packagereference-how-one-deleted-packagesconfig-took-down-my-ci-pipeline-4bm6</guid>
      <description>&lt;p&gt;Sometimes a migration doesn't start with a plan — it starts with a broken build. That's exactly what happened to me: a .NET Framework 4.8 solution with 17 projects, grown over years, suddenly started failing intermittently in Azure DevOps. What came out of it was a full migration from &lt;code&gt;packages.config&lt;/code&gt;/HintPath to &lt;code&gt;PackageReference&lt;/code&gt; — plus a whole cluster of side quests that I want to write down here, because each one of them would have cost an afternoon of debugging on its own if you didn't already know the cause. The build is green again — whether everything also behaves correctly at runtime still has to be verified by testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference: symptom → cause
&lt;/h2&gt;

&lt;p&gt;If you landed here with a specific error, here's the short version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Package not found" on restore, though the package is in the feed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;packages.config&lt;/code&gt; deleted, references still in HintPath style&lt;/td&gt;
&lt;td&gt;The actual root cause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore fails only for internal packages&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;&amp;lt;packageSources&amp;gt;&lt;/code&gt; in &lt;code&gt;NuGet.config&lt;/code&gt;, or feed retention&lt;/td&gt;
&lt;td&gt;First lead / Side quest 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Package not found" &lt;strong&gt;after&lt;/strong&gt; the migration&lt;/td&gt;
&lt;td&gt;Assembly name instead of package ID in &lt;code&gt;Include&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The trap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;NU1605&lt;/code&gt; – package downgrade detected&lt;/td&gt;
&lt;td&gt;Normal consequence of strict dependency resolution&lt;/td&gt;
&lt;td&gt;Expect NU1605&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build green, but the wrong target framework is active&lt;/td&gt;
&lt;td&gt;Targeting pack missing (runtime ≠ targeting pack)&lt;/td&gt;
&lt;td&gt;Side quest 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CA0063&lt;/code&gt; / &lt;code&gt;CA0064&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reference to a &lt;code&gt;.ruleset&lt;/code&gt; that no longer exists&lt;/td&gt;
&lt;td&gt;CA0063/CA0064&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shifting, non-reproducible restore errors&lt;/td&gt;
&lt;td&gt;Race condition from a parallel restore job&lt;/td&gt;
&lt;td&gt;Race condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CS8032&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Analyzer requires newer Roslyn than the agent has&lt;/td&gt;
&lt;td&gt;CS8032&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MSB3277&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conflicting assembly versions&lt;/td&gt;
&lt;td&gt;MSB3277&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Starting point
&lt;/h2&gt;

&lt;p&gt;The solution is a classic .NET Framework 4.8 codebase with 17 project files, referencing both third-party packages (EntityFramework 6.5.2, &lt;code&gt;Azure.Core&lt;/code&gt;, Enterprise Library, xunit, various &lt;code&gt;System.*&lt;/code&gt;) and internally maintained packages from a private NuGet feed. Build and deployment run through an Azure DevOps pipeline on self-hosted agents.&lt;/p&gt;

&lt;p&gt;The trigger: NuGet restore started failing sporadically in the pipeline with "package not found" errors for &lt;code&gt;EntityFramework.6.5.2&lt;/code&gt; and others — but not consistently across all projects. Locally on the dev machine, everything built just fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  First lead: missing restore instructions
&lt;/h2&gt;

&lt;p&gt;The first suspicion — a missing &lt;code&gt;NuGetToolInstaller&lt;/code&gt; or &lt;code&gt;NuGetCommand&lt;/code&gt; task ahead of the actual build step — turned out to be &lt;em&gt;part&lt;/em&gt; of the problem, but not the core of it. A look at the solution-local &lt;code&gt;NuGet.config&lt;/code&gt; revealed something else: it only contained &lt;code&gt;disableSourceControlIntegration&lt;/code&gt;, with no &lt;code&gt;&amp;lt;packageSources&amp;gt;&lt;/code&gt; section at all. That worked locally because the internal feed was configured globally on the dev machine — the build agent had no such global configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; package sources belong explicitly in the solution-local &lt;code&gt;NuGet.config&lt;/code&gt;, with a &lt;code&gt;&amp;lt;clear /&amp;gt;&lt;/code&gt; in front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;packageSources&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;clear&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"nuget.org"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"https://api.nuget.org/v3/index.json"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"internal-feed"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"https://.../index.json"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/packageSources&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;solution&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"disableSourceControlIntegration"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/solution&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;clear /&amp;gt;&lt;/code&gt; is the crucial part. Without it, whatever sources are configured globally on the machine get merged in. That's precisely how you end up in the "green locally, red on the agent" situation — your own machine carries years of accumulated global entries that the build agent simply doesn't have. With &lt;code&gt;&amp;lt;clear /&amp;gt;&lt;/code&gt;, the source list is identical and reproducible for anyone who checks out the repository.&lt;/p&gt;

&lt;p&gt;A quick way to see what's actually in effect on a given machine or agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;nuget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-All&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the real root cause sat deeper still.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual root cause: deleted &lt;code&gt;packages.config&lt;/code&gt; with HintPath references
&lt;/h2&gt;

&lt;p&gt;During an earlier cleanup, the &lt;code&gt;packages.config&lt;/code&gt; files of several projects had been deleted — while the &lt;code&gt;.csproj&lt;/code&gt; files still referenced packages in the old &lt;strong&gt;HintPath style&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Reference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"EntityFramework, Version=6.0.0.0, ..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;HintPath&amp;gt;&lt;/span&gt;..\packages\EntityFramework.6.5.2\lib\net45\EntityFramework.dll&lt;span class="nt"&gt;&amp;lt;/HintPath&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Reference&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this reference style, NuGet absolutely requires &lt;code&gt;packages.config&lt;/code&gt; as its restore instruction. Without it, NuGet has no information about what should be downloaded into the local &lt;code&gt;packages&lt;/code&gt; folder. The &lt;code&gt;HintPath&lt;/code&gt; keeps pointing at a folder that never gets populated — hence the seemingly random "package not found" errors, depending on what happened to already be sitting in that folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to spot it:&lt;/strong&gt; if a project contains &lt;code&gt;&amp;lt;Reference&amp;gt;&lt;/code&gt; entries with &lt;code&gt;&amp;lt;HintPath&amp;gt;..\packages\...&lt;/code&gt; but has no &lt;code&gt;packages.config&lt;/code&gt; in the project folder (anymore), that's the unambiguous fingerprint of this problem. A quick check across the whole solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.csproj"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DirectoryName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$hasHintPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;HintPath&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Quiet&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$hasPackagesConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"packages.config"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hasHintPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$hasPackagesConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Affected: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything this prints either needs its &lt;code&gt;packages.config&lt;/code&gt; back, or needs to be migrated to &lt;code&gt;PackageReference&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My call:&lt;/strong&gt; rather than restoring &lt;code&gt;packages.config&lt;/code&gt;, I went the cleaner, more modern route — migrating to &lt;code&gt;PackageReference&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why by hand and not with the migration assistant?
&lt;/h3&gt;

&lt;p&gt;The obvious question first: there &lt;em&gt;is&lt;/em&gt; a migration assistant — right-click &lt;code&gt;packages.config&lt;/code&gt; or the References node, "Migrate packages.config to PackageReference". So why do all of this manually?&lt;/p&gt;

&lt;p&gt;Two reasons, both of which applied here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The assistant is a Visual Studio feature&lt;/strong&gt; (VS 2017 15.7 and later). I work in JetBrains Rider, and that path wasn't available to me in my setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The assistant operates &lt;em&gt;on&lt;/em&gt; &lt;code&gt;packages.config&lt;/code&gt;&lt;/strong&gt; — it reads that file as its input to derive the package list. And that file had been deleted in exactly the affected projects. So even with Visual Studio, the standard tool wouldn't have been applicable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have an intact &lt;code&gt;packages.config&lt;/code&gt; and you're working in Visual Studio: use the assistant, it saves you all the manual work below. The manual route in this article is plan B for when one of those two conditions isn't met — and that's precisely when the package ID trap in the next section is where things go wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating to PackageReference
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PackageReference&lt;/code&gt; solves the structural problem for good: no more &lt;code&gt;packages.config&lt;/code&gt;, no HintPath synchronization, packages managed centrally in the global NuGet cache (&lt;code&gt;%userprofile%\.nuget\packages&lt;/code&gt;) and resolved through &lt;code&gt;obj\project.assets.json&lt;/code&gt;. This works with classic .NET Framework projects too — not just SDK-style ones — as long as you're on NuGet 4.0+ / VS2017+.&lt;/p&gt;

&lt;p&gt;Key settings I kept consistent across projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;RestoreProjectStyle&amp;gt;&lt;/span&gt;PackageReference&lt;span class="nt"&gt;&amp;lt;/RestoreProjectStyle&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;AutoGenerateBindingRedirects&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/AutoGenerateBindingRedirects&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;GenerateBindingRedirectsOutputType&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/GenerateBindingRedirectsOutputType&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Framework assemblies (&lt;code&gt;System.*&lt;/code&gt;, etc.) stay as classic &lt;code&gt;&amp;lt;Reference&amp;gt;&lt;/code&gt; entries — those aren't managed through NuGet.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;EnsureNuGetPackageBuildImports&lt;/code&gt; target block and all the legacy &lt;code&gt;.props&lt;/code&gt;/&lt;code&gt;.targets&lt;/code&gt; imports NuGet used to generate for the &lt;code&gt;packages.config&lt;/code&gt; style get removed.&lt;/li&gt;
&lt;li&gt;Binding redirects are now generated automatically instead of hand-maintained in &lt;code&gt;App.config&lt;/code&gt; — which matters when duplicate versions are floating around (e.g. &lt;code&gt;EntityFramework&lt;/code&gt; 6.0.0 and 6.5.2 both sitting in the old &lt;code&gt;packages&lt;/code&gt; folder).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Projects using EDMX/T4 code generation (EF6 Database First) needed extra attention, because the generated &lt;code&gt;.Designer.cs&lt;/code&gt; files had to keep working — the plain &lt;code&gt;PackageReference&lt;/code&gt; switch doesn't automatically cover that, so the codegen infrastructure needs to be verified alongside it.&lt;/p&gt;

&lt;p&gt;I migrated all 17 project files one at a time rather than risking a big-bang migration. That approach transfers to any grown .NET Framework codebase: convert one project, commit and build it locally, then move to the next. With five, six or more affected projects, this drastically cuts down debugging time, because a build failure immediately tells you which project caused it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical per-project workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Note every &lt;code&gt;&amp;lt;Reference&amp;gt;&lt;/code&gt; entry with a &lt;code&gt;&amp;lt;HintPath&amp;gt;&lt;/code&gt; (the version is reliably in the path — the package name is &lt;em&gt;not always&lt;/em&gt;, see the warning below).&lt;/li&gt;
&lt;li&gt;Replace the &lt;code&gt;&amp;lt;Reference&amp;gt;&lt;/code&gt; block with one &lt;code&gt;&amp;lt;PackageReference Include="PackageId" Version="x.y.z" /&amp;gt;&lt;/code&gt; per package.&lt;/li&gt;
&lt;li&gt;Leave &lt;code&gt;&amp;lt;Reference&amp;gt;&lt;/code&gt; entries for pure framework assemblies (no corresponding NuGet package, e.g. &lt;code&gt;System.Data&lt;/code&gt;) untouched.&lt;/li&gt;
&lt;li&gt;Remove the legacy NuGet-generated &lt;code&gt;.props&lt;/code&gt;/&lt;code&gt;.targets&lt;/code&gt; imports and the &lt;code&gt;EnsureNuGetPackageBuildImports&lt;/code&gt; target block.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;RestoreProjectStyle&lt;/code&gt;, &lt;code&gt;AutoGenerateBindingRedirects&lt;/code&gt; and &lt;code&gt;GenerateBindingRedirectsOutputType&lt;/code&gt; (see above).&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;nuget restore&lt;/code&gt; / &lt;code&gt;dotnet restore&lt;/code&gt; locally and check that &lt;code&gt;obj\project.assets.json&lt;/code&gt; is generated cleanly — that's the most reliable signal the switch worked for this project.&lt;/li&gt;
&lt;li&gt;Only then commit and move on to the next project.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The trap: assembly name is not package ID
&lt;/h3&gt;

&lt;p&gt;This is the mistake that cost me the most time — and it's insidious, because it looks entirely plausible at first glance.&lt;/p&gt;

&lt;p&gt;In the HintPath style, the &lt;code&gt;Include&lt;/code&gt; attribute holds the &lt;strong&gt;assembly name&lt;/strong&gt;. In &lt;code&gt;PackageReference&lt;/code&gt;, it needs the &lt;strong&gt;NuGet package ID&lt;/strong&gt;. For most packages these are identical (&lt;code&gt;EntityFramework&lt;/code&gt; is called the same as an assembly and as a package) — but not for all of them. An example from my solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- old: assembly name --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Reference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Practices.EnterpriseLibrary.Common"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;HintPath&amp;gt;&lt;/span&gt;..\packages\EnterpriseLibrary.Common.6.0.1304\lib\NET45\Microsoft.Practices.EnterpriseLibrary.Common.dll&lt;span class="nt"&gt;&amp;lt;/HintPath&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Reference&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- new: package ID --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"EnterpriseLibrary.Common"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"6.0.1304"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you mechanically carry over the &lt;code&gt;Include&lt;/code&gt; value, NuGet goes looking for a package called &lt;code&gt;Microsoft.Practices.EnterpriseLibrary.Common&lt;/code&gt; — which doesn't exist in the feed. Result: another "package not found", this time self-inflicted. In my case this error propagated through 16 files and had to be undone with a batch replace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reliable source for the package ID is the folder name in the &lt;code&gt;packages&lt;/code&gt; path&lt;/strong&gt;, not the &lt;code&gt;Include&lt;/code&gt; attribute: &lt;code&gt;..\packages\EnterpriseLibrary.Common.6.0.1304\...&lt;/code&gt; → package ID &lt;code&gt;EnterpriseLibrary.Common&lt;/code&gt;, version &lt;code&gt;6.0.1304&lt;/code&gt;. That folder is always named &lt;code&gt;&amp;lt;PackageId&amp;gt;.&amp;lt;Version&amp;gt;&lt;/code&gt;. So if you script the migration, parse the path — don't copy the &lt;code&gt;Include&lt;/code&gt; attribute.&lt;/p&gt;

&lt;h3&gt;
  
  
  NU1605: why downgrade warnings show up after the migration
&lt;/h3&gt;

&lt;p&gt;After the switch, several projects started producing &lt;code&gt;NU1605&lt;/code&gt; warnings ("detected package downgrade"). This isn't an edge case — it's the normal consequence of migrating. &lt;code&gt;packages.config&lt;/code&gt; never strictly validated transitive dependencies; each project simply pulled whatever was in its own list. &lt;code&gt;PackageReference&lt;/code&gt; resolves the full dependency graph strictly and flags every conflict where a direct reference forces an older version than a transitive dependency requires.&lt;/p&gt;

&lt;p&gt;In practice that means: after migrating, version conflicts surface that were silently baked into the build before. The fix is usually unspectacular — raise all affected projects to the same higher, consistent version set. The important thing is not to be blindsided by it: these aren't new problems the migration created, they're old inconsistencies it made visible.&lt;/p&gt;

&lt;p&gt;Once all projects were migrated and the versions aligned, &lt;code&gt;dotnet restore&lt;/code&gt; completed with 0 errors and 0 warnings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup: the &lt;code&gt;packages&lt;/code&gt; folder is dead
&lt;/h3&gt;

&lt;p&gt;One detail that tends to get left behind: after the migration, the solution-local &lt;code&gt;packages&lt;/code&gt; folder is no longer populated and no longer read — all packages now live in the global cache under &lt;code&gt;%userprofile%\.nuget\packages&lt;/code&gt;. So the old folder can be deleted, as soon as &lt;strong&gt;all&lt;/strong&gt; projects have been converted. As long as one project is still on the HintPath style, that one still needs it.&lt;/p&gt;

&lt;p&gt;If the folder was versioned until now, it also belongs in &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# NuGet: no longer needed after the PackageReference migration
packages/
*.nupkg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And one safety net that sounds trivial but makes all the difference with 17 project files in play: the whole conversion ran on a feature branch, not on the main branch. That makes every intermediate state throwaway-able, which is what gives you the nerve to rewrite a whole file instead of cautiously patching around in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side quest 1: the silently downgrading framework compiler
&lt;/h2&gt;

&lt;p&gt;In parallel, the question came up whether bumping from &lt;code&gt;v4.8&lt;/code&gt; to &lt;code&gt;v4.8.1&lt;/code&gt; would be worthwhile. The runtime was there — confirmed via the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Release&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# 533509  → corresponds to .NET Framework 4.8.1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second check, however, was sobering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# False&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;And this is the part that's easy to miss: the runtime and the targeting pack are two different things.&lt;/strong&gt; The runtime only tells you that applications targeting 4.8.1 can &lt;em&gt;run&lt;/em&gt; on this machine. To &lt;em&gt;compile against&lt;/em&gt; 4.8.1, you additionally need the reference assemblies from the developer/targeting pack. So "but 4.8.1 is installed" — whether from Windows Features or the registry — doesn't answer the compile-time question at all.&lt;/p&gt;

&lt;p&gt;That's why, even though the &lt;code&gt;.csproj&lt;/code&gt; already declared &lt;code&gt;v4.8.1&lt;/code&gt; as its target, MSBuild &lt;strong&gt;silently&lt;/strong&gt; fell back to &lt;code&gt;v4.8&lt;/code&gt; — no error, no warning. This is easy to miss precisely because the build stays green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm it if you suspect it:&lt;/strong&gt; search the MSBuild log (verbosity at least "Detailed") for the &lt;code&gt;csc.exe&lt;/code&gt; invocation line — the reference paths will point at either &lt;code&gt;...\.NETFramework\v4.8\...&lt;/code&gt; or &lt;code&gt;...\.NETFramework\v4.8.1\...&lt;/code&gt;. Equally telling: the generated file in the &lt;code&gt;obj&lt;/code&gt; folder named along the lines of &lt;code&gt;.NETFramework,Version=v4.8.1.AssemblyAttributes.cs&lt;/code&gt; — the version part of that filename reflects the version actually used, regardless of what the &lt;code&gt;.csproj&lt;/code&gt; claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix if the pack is missing:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the &lt;a href="https://dotnet.microsoft.com/en-us/download/dotnet-framework/net481" rel="noopener noreferrer"&gt;.NET Framework 4.8.1 Developer Pack&lt;/a&gt; (requires admin rights).&lt;/li&gt;
&lt;li&gt;Verify afterwards: &lt;code&gt;Test-Path "C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1"&lt;/code&gt; should now return &lt;code&gt;True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fully restart the IDE (not just reload the solution) — the reference assembly cache is read at IDE startup.&lt;/li&gt;
&lt;li&gt;Clean and full rebuild, not just incremental, so the generated &lt;code&gt;AssemblyAttributes.cs&lt;/code&gt; files are recreated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One important caveat: installing the developer/targeting pack locally has &lt;strong&gt;zero effect&lt;/strong&gt; on your Azure DevOps build agents. That has to be handled separately — for Microsoft-hosted agents, check the image documentation for which frameworks come preinstalled; for self-hosted agents, install the developer pack there manually as well. Projects still targeting 4.8 keep working unaffected, since 4.8.1 is backwards compatible. In the end the conclusion was clear: the 4.8.1 bump is a nice-to-have with no urgency — the actual priority remained the restore fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side quest 2: internal package versions and feed retention
&lt;/h2&gt;

&lt;p&gt;On top of that, restore errors were showing up exclusively for internally maintained packages — not for EntityFramework or other third-party packages, which verified cleanly. Possible causes I worked through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The package version was never successfully pushed to the feed (failed publish pipeline).&lt;/li&gt;
&lt;li&gt;A typo or wrong version in the reference.&lt;/li&gt;
&lt;li&gt;Feed retention/cleanup: some internal feeds automatically prune old, unused package versions.&lt;/li&gt;
&lt;li&gt;Wrong feed scope (e.g. hitting the release view when the version only exists as prerelease).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Concrete checks before you go hunting through your own code:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look directly in the feed (the provider's web UI, or &lt;code&gt;nuget list &amp;lt;PackageName&amp;gt; -Source &amp;lt;FeedUrl&amp;gt; -AllVersions -PreRelease&lt;/code&gt;) to see whether the referenced version actually still exists.&lt;/li&gt;
&lt;li&gt;Verify that the build/publish pipeline meant to produce that package version actually &lt;em&gt;succeeded&lt;/em&gt; — not just that it was triggered.&lt;/li&gt;
&lt;li&gt;Compare the version in source (&lt;code&gt;.csproj&lt;/code&gt;, or previously &lt;code&gt;packages.config&lt;/code&gt;) against what's available in the feed, exactly — including casing and suffixes like &lt;code&gt;-beta&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the feed has multiple views (e.g. "Release" vs. "Prerelease"/"Local"), make sure the build agent is hitting the view that actually contains the version you need.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful reminder: with internal feeds, do this comparison &lt;em&gt;first&lt;/em&gt;, before spending days looking for the bug in your own code — the cause is almost never in the code itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side quest 3: several pipeline problems in a single afternoon
&lt;/h2&gt;

&lt;p&gt;Once the project structure was in place, the first full pipeline run surfaced several independent problems all at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  CA0063/CA0064: stale ruleset reference
&lt;/h3&gt;

&lt;p&gt;Several &lt;code&gt;.csproj&lt;/code&gt; files still had &lt;code&gt;&amp;lt;CodeAnalysisRuleSet&amp;gt;&lt;/code&gt; pointing at a &lt;code&gt;.ruleset&lt;/code&gt; file that no longer existed, producing CA0063 ("ruleset file not found") and CA0064 warnings. You can spot this directly in the build output from those exact warning codes plus the path to the missing file. Fix: remove the &lt;code&gt;&amp;lt;CodeAnalysisRuleSet&amp;gt;&lt;/code&gt; property from the affected &lt;code&gt;.csproj&lt;/code&gt; files — either individually, or more cleanly by managing a shared analyzer/ruleset configuration centrally in a &lt;code&gt;Directory.Build.props&lt;/code&gt; at the solution root instead of maintaining it per project.&lt;/p&gt;

&lt;h3&gt;
  
  
  YAML syntax error: the misindented task
&lt;/h3&gt;

&lt;p&gt;A copy-paste mistake had accidentally indented a &lt;code&gt;NuGetCommand@2&lt;/code&gt; task underneath the &lt;code&gt;script:&lt;/code&gt; scalar of a PowerShell task — a classic, hard-to-see YAML indentation bug. Azure DevOps usually reports these with a line number in the parse error, but the actual cause (wrong indentation depth) often sits a few lines above or below it. Worth doing: run the YAML through a linter/validator (Azure DevOps has a built-in YAML preview, or use an external YAML linter) before kicking off another build — it saves you waiting on the agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Race condition from a parallel restore job
&lt;/h3&gt;

&lt;p&gt;A separate &lt;code&gt;Restore&lt;/code&gt; job ran in parallel with the template-based build job, both hitting the same &lt;code&gt;.nuget\packages&lt;/code&gt; folder — a textbook race condition. The build job template already brought its own restore step, so the separate job could simply be removed. A tell-tale sign of this kind of race condition in the logs: shifting, non-reproducible restore errors that come and go across re-runs of the &lt;em&gt;same&lt;/em&gt; commit. That's unusual for a structural problem (like the deleted &lt;code&gt;packages.config&lt;/code&gt;), so it points toward timing/concurrency instead. In general, when using pipeline templates, look inside them to see which restore/build steps they already provide before adding your own redundant jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side quest 4: CS8032 — when the analyzer is newer than the compiler
&lt;/h2&gt;

&lt;p&gt;The most stubborn error came last: &lt;code&gt;CS8032&lt;/code&gt; when building an EF6/EDMX project that had no modern dependencies of its own. The error message names the required &lt;code&gt;Microsoft.CodeAnalysis&lt;/code&gt; version — that's your most important clue. In my case, &lt;code&gt;Azure.Core&lt;/code&gt; 1.60.0 transitively pulled in &lt;code&gt;System.ClientModel&lt;/code&gt; 1.14.0, and that package ships Roslyn analyzers requiring &lt;code&gt;Microsoft.CodeAnalysis&lt;/code&gt; 4.3.0.0. The build agent, however, was still on a VS 2019 toolset with Roslyn 3.11 — locally, with a newer toolset, the error never showed up. Same mechanism as the targeting pack: it isn't the code that differs, it's the environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to find out which package brings the analyzer:&lt;/strong&gt; search the global NuGet cache (&lt;code&gt;%userprofile%\.nuget\packages&lt;/code&gt;) for &lt;code&gt;analyzers\dotnet\cs\*.dll&lt;/code&gt; — the package folder above it identifies the culprit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\.nuget\packages"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"analyzers"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExpandProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SDK-style projects, &lt;code&gt;dotnet list &amp;lt;project&amp;gt;.csproj package --include-transitive&lt;/code&gt; also works; for classic, non-SDK-style &lt;code&gt;.csproj&lt;/code&gt; files — which is exactly the case here — it generally does &lt;strong&gt;not&lt;/strong&gt; work reliably. That's why going through the package cache is the more dependable route in this scenario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — surgical, when only one or two projects are affected:&lt;/strong&gt; reference the offending package explicitly and exclude only its analyzers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"System.ClientModel"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.14.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ExcludeAssets&amp;gt;&lt;/span&gt;analyzers&lt;span class="nt"&gt;&amp;lt;/ExcludeAssets&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cleaner solution, because analyzers from every other package keep running. The downside: you have to locate and touch each affected project individually — and with a transitively pulled package, that's usually more projects than you'd expect. In my case, the error popped right back up in the next project after the first fix, then the one after that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — global, when the problem runs through the whole solution:&lt;/strong&gt; since upgrading the agent toolset wasn't something I could do on short notice (the build server is owned by a different department), the pragmatic solution was a central &lt;code&gt;Directory.Build.props&lt;/code&gt; at the solution root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;RunAnalyzersDuringBuild&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/RunAnalyzersDuringBuild&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important — and this is where I went down the wrong path at first:&lt;/strong&gt; the obvious reflex is &lt;code&gt;&amp;lt;NoWarn&amp;gt;$(NoWarn);CS8032&amp;lt;/NoWarn&amp;gt;&lt;/code&gt;. That does nothing here. CS8032 shows up in the log as an &lt;strong&gt;error&lt;/strong&gt; (&lt;code&gt;##[error]CSC(0,0): Error CS8032&lt;/code&gt;), and &lt;code&gt;NoWarn&lt;/code&gt; only suppresses diagnostics classified as warnings. For a hard compiler error — analyzer instantiation fails — it simply doesn't apply. The actual effect comes from &lt;code&gt;RunAnalyzersDuringBuild=false&lt;/code&gt;, because that stops &lt;code&gt;csc&lt;/code&gt; from loading the analyzer DLLs in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where &lt;code&gt;Directory.Build.props&lt;/code&gt; has to live:&lt;/strong&gt; MSBuild searches upward from each project folder and stops at the &lt;strong&gt;first match&lt;/strong&gt;. So it has to sit somewhere that is a parent directory of all affected project folders — usually next to the &lt;code&gt;.sln&lt;/code&gt;. A common stumbling block: some IDEs don't show the file in the Solution Explorer automatically. That's normal and doesn't mean it isn't taking effect — it doesn't need to be included as a solution item.&lt;/p&gt;

&lt;p&gt;The big advantage of the global approach: you don't have to hunt down and patch every single project that transitively pulls in the problematic package — including the ones you haven't even noticed yet. After adding it, still check the log to confirm CS8032 is gone for &lt;strong&gt;all&lt;/strong&gt; projects, not just the one you started with.&lt;/p&gt;

&lt;h3&gt;
  
  
  MSB3277: conflicting assembly versions
&lt;/h3&gt;

&lt;p&gt;At the same time, an &lt;code&gt;MSB3277&lt;/code&gt; warning appeared about conflicting &lt;code&gt;System.Text.Json&lt;/code&gt; versions. Same root cause — overly modern packages pulled in transitively into a net48 project — and it can be defused via the automatically generated binding redirects mentioned earlier. If you're seeing CS8032, search the same log for MSB3277 too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a green build does not prove
&lt;/h2&gt;

&lt;p&gt;An honest status update at this point: the build passes, and &lt;code&gt;dotnet restore&lt;/code&gt; reports 0 errors and 0 warnings. What that does &lt;strong&gt;not&lt;/strong&gt; prove is that everything works correctly at runtime. The compiler checks references and syntax — it doesn't start the application. Verifying the behaviour is still ahead of me.&lt;/p&gt;

&lt;p&gt;This migration in particular shifts several things that only surface at runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binding redirects&lt;/strong&gt; are now generated automatically instead of being hand-maintained in &lt;code&gt;App.config&lt;/code&gt;. If the generated version differs from the old one, you won't find out at compile time — you'll find out as a &lt;code&gt;FileLoadException&lt;/code&gt; or &lt;code&gt;FileNotFoundException&lt;/code&gt; the first time the affected assembly is loaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raised package versions&lt;/strong&gt; (the NU1605 resolution) mean newer code is now running in several places. It compiles — whether behaviour changed is not something the build can tell you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EF6 loads its provider at runtime&lt;/strong&gt; via &lt;code&gt;App.config&lt;/code&gt;. If an entry is missing after the switch, or points at a different version, you'll discover it on the first database access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content files from packages&lt;/strong&gt; were sometimes copied to the output directory differently under &lt;code&gt;packages.config&lt;/code&gt; than under &lt;code&gt;PackageReference&lt;/code&gt;. Again, purely a runtime concern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's on the verification list:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Actually start the application, not just build it.&lt;/li&gt;
&lt;li&gt;Trigger a real database access through the EF context.&lt;/li&gt;
&lt;li&gt;Diff the generated &lt;code&gt;.config&lt;/code&gt; in the output directory (&lt;code&gt;bin\...\*.exe.config&lt;/code&gt;) against the old, hand-maintained version — that shows at a glance which binding redirects were added, dropped, or repointed.&lt;/li&gt;
&lt;li&gt;Run the test suite, paying particular attention to the projects whose package versions were raised.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sounds obvious, but in practice it isn't: after a week of a red pipeline, the temptation to file the green checkmark as "done" is considerable. It's really just the point where the actual testing can begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;What started as a single "package not found" error turned out to be a chain of independent but mutually reinforcing problems: a structural error (deleted &lt;code&gt;packages.config&lt;/code&gt; with HintPath references), an environment gap between the local machine and the build agent (targeting pack, toolset version, NuGet sources), a race condition in the pipeline design, and several smaller configuration mistakes — plus one self-inflicted wound from confusing assembly names with package IDs. The build is green now; the runtime verification, as described above, is still pending.&lt;/p&gt;

&lt;p&gt;The main lesson: &lt;strong&gt;green locally does not mean CI-ready.&lt;/strong&gt; With grown .NET Framework solutions in particular, it pays to explicitly ask what only works on your own machine "by accident", because something is configured globally somewhere that the build agent doesn't have. Three of the problems described here — missing package sources, missing targeting pack, outdated Roslyn toolset — are the exact same mechanism wearing three different costumes. Once you've recognized that pattern, the next "but it works on my machine" doesn't send you into the code first.&lt;/p&gt;

&lt;p&gt;In the end, migrating to &lt;code&gt;PackageReference&lt;/code&gt; wasn't just the fix for the immediate problem — it made the entire restore chain substantially more robust: no more path dependencies, no more double bookkeeping between &lt;code&gt;.csproj&lt;/code&gt; and &lt;code&gt;packages.config&lt;/code&gt;, and transitive dependencies resolved automatically instead of hundreds of individually maintained references. The NU1605 warnings that surfaced along the way weren't a cost of the migration — they were the migration doing its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Central Package Management.&lt;/strong&gt; With 17 projects on &lt;code&gt;PackageReference&lt;/code&gt;, every package version now lives in 17 files — exactly the problem I just solved by hand while aligning the NU1605 conflicts. With a &lt;code&gt;Directory.Packages.props&lt;/code&gt; and &lt;code&gt;ManagePackageVersionsCentrally&lt;/code&gt;, each version is declared once centrally, and the &lt;code&gt;.csproj&lt;/code&gt; only carries &lt;code&gt;&amp;lt;PackageReference Include="..." /&amp;gt;&lt;/code&gt; without a version. The next version bump becomes one line instead of seventeen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lockfiles.&lt;/strong&gt; &lt;code&gt;RestorePackagesWithLockFile&lt;/code&gt; writes the resolved dependency graph into a &lt;code&gt;packages.lock.json&lt;/code&gt; that gets committed alongside the code. That makes restore on the build agent bit-for-bit identical to the local one — and any divergence surfaces as an error instead of disguising itself as "but it works on my machine". For an article whose whole thread is that exact difference, it's the logical endpoint.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;On how this came about: I worked through the debugging and the migration with the support of an AI assistant, and used it again to assemble this article from my working notes. Every error, fix and dead end described here comes from the actual course of the project — including the places where we took a wrong turn together.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>azuredevops</category>
      <category>nuget</category>
    </item>
    <item>
      <title>NuGet Restore Failing with 'Unable to find version' Package? Check Your NuGetToolInstaller Version!</title>
      <dc:creator>S8ui</dc:creator>
      <pubDate>Mon, 17 Aug 2026 12:34:31 +0000</pubDate>
      <link>https://dev.to/sui23e879d/nuget-restore-failing-with-unable-to-find-version-package-check-your-nugettoolinstaller-version-2e9l</link>
      <guid>https://dev.to/sui23e879d/nuget-restore-failing-with-unable-to-find-version-package-check-your-nugettoolinstaller-version-2e9l</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In one of our Azure DevOps pipelines, nuget restore suddenly started failing with an error stating, in essence, that the requested package could not be found in the referenced version. The task referencing the package hadn't changed — yet the restore stage kept failing.&lt;/p&gt;

&lt;p&gt;At first glance, this looks like an issue with the package source, some caching effect, or a broken .nuspec/lockfile. It wasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Root Cause&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The actual culprit was the version of the NuGetToolInstaller@1 task itself. The pipeline had NuGet pinned to version 6.12.2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bump the versionSpec in the NuGetToolInstaller@1 task from 6.12.2 to 7.9.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGetToolInstaller@1&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NuGet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;7.9.0'&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;versionSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7.9.0&lt;/span&gt;
      &lt;span class="na"&gt;checkLatest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. After the update, nuget restore ran through cleanly again.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>nuget</category>
      <category>azuredevops</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
