Skip to content

[CLI] Load MSBuild assemblies from the .NET SDK, never from NuGet - #1783

Merged
josefpihrt merged 2 commits into
mainfrom
fix/cli-msbuild-assemblies-from-sdk
Aug 7, 2026
Merged

[CLI] Load MSBuild assemblies from the .NET SDK, never from NuGet#1783
josefpihrt merged 2 commits into
mainfrom
fix/cli-msbuild-assemblies-from-sdk

Conversation

@josefpihrt

Copy link
Copy Markdown
Collaborator

Summary

The CLI deploys its own copies of MSBuild's assemblies. Those copies always win over the ones in the user's .NET SDK, so the SDK's MSBuild engine ends up running against a different build of its own halves. On SDK 10 patches carrying MSBuild 18.6 or later this fails immediately with TypeLoadException: Could not load type 'Microsoft.Build.Framework.FileUtilities'.

This makes the tool stop deploying MSBuild entirely and lets MSBuildLocator resolve the whole set from the SDK, for net8.0, net9.0 and net10.0.

Fixes #1729, #1748, #1716. Supersedes #1762 — the diagnosis there is what led here, thanks @charles8051.

Root cause

Roslynator cannot ship MSBuild: opening a project requires the user's MSBuild, because that is what knows their SDK's targets. So MSBuildWorkspaceCommand registers MSBuildLocator, which resolves MSBuild out of the installed SDK.

MSBuildLocator hooks AssemblyLoadContext.Resolving, and that only fires when an assembly is not found next to the app. Any copy we deploy therefore wins silently and the locator is never consulted for it.

ExcludeAssets="runtime" was set on Microsoft.Build, but that setting is not inherited by a package's dependencies. From project.assets.json before this change:

Microsoft.Build/18.0.2            runtime: ['lib/net10.0/_._']                        ← excluded
Microsoft.Build.Framework/18.0.2  runtime: ['lib/net10.0/Microsoft.Build.Framework.dll'] ← deployed

Three more were deployed the same way, pulled in transitively by Microsoft.CodeAnalysis.Workspaces.MSBuild: Microsoft.Build.Tasks.Core and Microsoft.Build.Utilities.Core at 17.7.2, plus Microsoft.NET.StringTools.

MSBuild's assemblies are a matched set — versioned together and coupled through InternalsVisibleTo — so mixing builds is the defect. Concretely, MSBuild moved shared helpers into Microsoft.Build.Framework between 18.4 and 18.6:

Microsoft.Build.Framework lib size contains FileUtilities
18.0.2 269,608 no
18.4.0 305,424 no
18.6.3 529,712 yes
18.8.2 1,051,472 yes

An SDK whose engine is 18.6+ asks for a type that the deployed 18.0.2 build does not have. Nothing changed on Roslynator's side; MSBuild 17's solution parser simply never called across into Framework internals on that path, and 18's .slnx parser does.

Note this is not specific to .slnx#1729 reports MissingFieldException on a plain console project. It is general assembly shadowing, which is why net8.0 is covered here too: the TFM of an installed tool is frozen at install time, so anyone who installed under SDK 8 or 9 and later moved to SDK 10 keeps that asset and hits the same mismatch.

Why not just bump the MSBuild version

Because it would only hide this particular crash. To make deploying MSBuild work you would have to match the version on the user's machine, and that is unknowable — their SDK patch level varies per machine and moves on Microsoft's schedule, not Roslynator's. Any version chosen is wrong for someone, and the next SDK patch that moves anything internal breaks it again. Deploying none of them is the only stable answer.

The fix

All five packages are referenced with ExcludeAssets="runtime", so only the compile-time reference assemblies are used and nothing is deployed. net48 is untouched — it resolves MSBuild from a Visual Studio install and has a different risk profile.

Versions are pinned to exactly what the restore graph already resolves, which keeps the deployed dependency set byte-identical. Two things ruled out the tidier alternatives:

  • Raising Tasks.Core/Utilities.Core to match the others pulls in System.Security.Cryptography.Xml 9.0.0, which has open advisories (32 new NU1903 warnings).
  • Collapsing to one version for all TFMs downgrades deployed packages — System.Reflection.MetadataLoadContext 9.0.0 → 7.0.0 on net9.0 and net10.0.

Since these are compile-time references only, their contents no longer matter; the comments in the file record why the values are what they are.

Regression guard

A new build target fails the build if any of these assemblies is about to be deployed. This class of bug is invisible at build time and only shows up as a crash on a user's machine, and a future Roslyn or MSBuild bump could reintroduce a copy at any point. Verified to fire:

error : These assemblies must be loaded from the .NET SDK at run time, but they are about to be
deployed with the tool: Microsoft.Build.Framework.dll. Add a PackageReference with
ExcludeAssets="runtime" for whichever package now brings them in.

Testing

Check Result
Restore graph vs. main identical, all three TFMs
New advisory warnings none
Build net8.0 / net9.0 / net10.0 0 errors, 0 warnings
Microsoft.Build* / StringTools in output none, all three TFMs
Dangling deps.json entries none
list-symbols on a .csproj exit 0, identical output on all three TFMs
list-symbols on a .slnx exit 0, net9.0 and net10.0
MSBuild resolved from SDK 9.0.302 (net9 build), SDK 10.0.101 (net10 build)
Guard fires when an exclusion is removed yes, all three TFMs

Not verified: the SDK available here is 10.0.101, which ships MSBuild 18.0.6 — before the 18.6 change. So this proves the assemblies are gone and nothing regressed, but not that the reported TypeLoadException disappears. That needs a check on an SDK 10 patch with MSBuild 18.6 or later. @charles8051, if you still have the environment from #1762, would you mind confirming?

The tool deploys its own copies of Microsoft.Build.Framework,
Microsoft.Build.Tasks.Core, Microsoft.Build.Utilities.Core and
Microsoft.NET.StringTools. The runtime prefers an assembly found next to
the app over anything MSBuildLocator can resolve, so those copies always
win and the SDK's are never loaded.

MSBuild's assemblies are a matched set that is versioned and internally
coupled. The engine loaded from the user's SDK therefore runs against a
different build of its own halves, which fails as TypeLoadException or
MissingMethodException as soon as the two diverge. MSBuild moved shared
helpers into Microsoft.Build.Framework between 18.4 and 18.6, so an SDK 10
patch carrying MSBuild 18.6 or later crashes against the deployed 18.0.2
copy.

Reference all five packages with ExcludeAssets="runtime" so that none of
them are deployed and MSBuildLocator resolves the whole set from the SDK.
Version matching is not a fix: the user's SDK patch level is unknown and
moves independently of Roslynator's releases.

Versions are pinned to what the restore graph already resolves, so the
deployed dependency set is unchanged. Raising Tasks.Core/Utilities.Core
would pull in System.Security.Cryptography.Xml 9.0.0, which has open
advisories, and collapsing to a single version would downgrade
System.Reflection.MetadataLoadContext.

A build target now fails the build if any of these assemblies is about to
be deployed, so a future dependency change cannot reintroduce this
silently.

Fixes #1729, #1748, #1716.

Co-Authored-By: Charles Lee <3188143+charles8051@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread ChangeLog.md Outdated
@charles8051

Copy link
Copy Markdown
Contributor

@josefpihrt

Confirmed working on dotnet SDK 10.0.301 and MSBuild 18.6.4

TypeLoadException has disappeared.

Thanks for pulling this in. Glad I could help even though I got it a bit wrong!

@josefpihrt

Copy link
Copy Markdown
Collaborator Author

@charles8051 thanks for testing it.

@josefpihrt
josefpihrt merged commit 35c8d83 into main Aug 7, 2026
17 checks passed
@josefpihrt
josefpihrt deleted the fix/cli-msbuild-assemblies-from-sdk branch August 7, 2026 23:26
This was referenced Aug 8, 2026
renebentes pushed a commit to renebentes/3054 that referenced this pull request Aug 13, 2026
Updated [Roslynator.Analyzers](https://github.com/dotnet/roslynator)
from 4.15.0 to 4.16.0.

<details>
<summary>Release notes</summary>

_Sourced from [Roslynator.Analyzers's
releases](https://github.com/dotnet/roslynator/releases)._

## 4.16.0

### Added

- [CLI] Suppress error code from Roslynator when it detects issues in
code but runs successfully
([PR](dotnet/roslynator#1756) by @​mdrybak)

### Fixed

- Fix analyzer
[RCS1118](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1118)
to not report local variable passed as 'in' argument
([PR](dotnet/roslynator#1782) by @​NoahStolk)
- Fix analyzer
[RCS1074](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1074)
([PR](dotnet/roslynator#1768) by @​cbersch)
- Fix enum contained flags check for partial matches in
[RCS1258](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1258)
([PR](dotnet/roslynator#1740) by @​ovska)
- Fix analyzer
[RCS1146](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1146)
([PR](dotnet/roslynator#1747))
- Fix analyzer
[RCS1194](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1194)
([PR](dotnet/roslynator#1733))
- Fix analyzer
[RCS1060](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1060)
to ignore classes marked with `file` modifier
([PR](dotnet/roslynator#1777) by @​cbersch)
- Fix analyzer
[RCS1231](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1231)
([PR](dotnet/roslynator#1774) by @​cbersch)
- Fix analyzer
[RCS1246](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1246)
for conditional access expressions
([PR](dotnet/roslynator#1772 by @​krajek))
- [CLI] Fix `fix` command ignoring `--include` / `--exclude` file filter
([PR](dotnet/roslynator#1758) by @​hashiiiii)
- [CLI] Fix loading of projects and solutions on .NET 10 SDK
([PR](dotnet/roslynator#1783))


Commits viewable in [compare
view](dotnet/roslynator@v4.15.0...v4.16.0).
</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
renebentes pushed a commit to renebentes/3054 that referenced this pull request Aug 13, 2026
…232)

Updated
[Roslynator.CodeAnalysis.Analyzers](https://github.com/dotnet/roslynator)
from 4.15.0 to 4.16.0.

<details>
<summary>Release notes</summary>

_Sourced from [Roslynator.CodeAnalysis.Analyzers's
releases](https://github.com/dotnet/roslynator/releases)._

## 4.16.0

### Added

- [CLI] Suppress error code from Roslynator when it detects issues in
code but runs successfully
([PR](dotnet/roslynator#1756) by @​mdrybak)

### Fixed

- Fix analyzer
[RCS1118](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1118)
to not report local variable passed as 'in' argument
([PR](dotnet/roslynator#1782) by @​NoahStolk)
- Fix analyzer
[RCS1074](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1074)
([PR](dotnet/roslynator#1768) by @​cbersch)
- Fix enum contained flags check for partial matches in
[RCS1258](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1258)
([PR](dotnet/roslynator#1740) by @​ovska)
- Fix analyzer
[RCS1146](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1146)
([PR](dotnet/roslynator#1747))
- Fix analyzer
[RCS1194](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1194)
([PR](dotnet/roslynator#1733))
- Fix analyzer
[RCS1060](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1060)
to ignore classes marked with `file` modifier
([PR](dotnet/roslynator#1777) by @​cbersch)
- Fix analyzer
[RCS1231](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1231)
([PR](dotnet/roslynator#1774) by @​cbersch)
- Fix analyzer
[RCS1246](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1246)
for conditional access expressions
([PR](dotnet/roslynator#1772 by @​krajek))
- [CLI] Fix `fix` command ignoring `--include` / `--exclude` file filter
([PR](dotnet/roslynator#1758) by @​hashiiiii)
- [CLI] Fix loading of projects and solutions on .NET 10 SDK
([PR](dotnet/roslynator#1783))


Commits viewable in [compare
view](dotnet/roslynator@v4.15.0...v4.16.0).
</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
renebentes pushed a commit to renebentes/3054 that referenced this pull request Aug 13, 2026
Updated
[Roslynator.Formatting.Analyzers](https://github.com/dotnet/roslynator)
from 4.15.0 to 4.16.0.

<details>
<summary>Release notes</summary>

_Sourced from [Roslynator.Formatting.Analyzers's
releases](https://github.com/dotnet/roslynator/releases)._

## 4.16.0

### Added

- [CLI] Suppress error code from Roslynator when it detects issues in
code but runs successfully
([PR](dotnet/roslynator#1756) by @​mdrybak)

### Fixed

- Fix analyzer
[RCS1118](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1118)
to not report local variable passed as 'in' argument
([PR](dotnet/roslynator#1782) by @​NoahStolk)
- Fix analyzer
[RCS1074](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1074)
([PR](dotnet/roslynator#1768) by @​cbersch)
- Fix enum contained flags check for partial matches in
[RCS1258](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1258)
([PR](dotnet/roslynator#1740) by @​ovska)
- Fix analyzer
[RCS1146](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1146)
([PR](dotnet/roslynator#1747))
- Fix analyzer
[RCS1194](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1194)
([PR](dotnet/roslynator#1733))
- Fix analyzer
[RCS1060](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1060)
to ignore classes marked with `file` modifier
([PR](dotnet/roslynator#1777) by @​cbersch)
- Fix analyzer
[RCS1231](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1231)
([PR](dotnet/roslynator#1774) by @​cbersch)
- Fix analyzer
[RCS1246](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1246)
for conditional access expressions
([PR](dotnet/roslynator#1772 by @​krajek))
- [CLI] Fix `fix` command ignoring `--include` / `--exclude` file filter
([PR](dotnet/roslynator#1758) by @​hashiiiii)
- [CLI] Fix loading of projects and solutions on .NET 10 SDK
([PR](dotnet/roslynator#1783))


Commits viewable in [compare
view](dotnet/roslynator@v4.15.0...v4.16.0).
</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI tool give MissingFieldException on Console app template

2 participants