MSBuild Integration Changes for .NET 10
Overview
EasyAF 4.0 includes significant changes to howEasyAF.MSBuild integrates with the MSBuild SDK. These changes were necessary to ensure compatibility across .NET 8, .NET 9, and .NET 10 when using MSBuildLocator to dynamically load MSBuild assemblies.
The Problem
Background: How MSBuildLocator Works
MSBuildLocator is a Microsoft library that allows applications to dynamically discover and load the MSBuild assemblies from an installed .NET SDK, rather than shipping specific versions of MSBuild with your application. This is the recommended approach for tools that need to manipulate .csproj files.
The workflow is:
- Call
MSBuildLocator.RegisterInstance()orRegisterDefaults()before any MSBuild types are accessed - MSBuildLocator sets up assembly resolution to redirect
Microsoft.Build.*assembly loads to the SDK’s copies - Your code can then use MSBuild APIs, and the correct SDK assemblies are loaded
What Broke in .NET 10
When running on .NET 10 SDK with .NET 8 as the target framework, tests began failing with errors like:Microsoft.Build NuGet packages were being copied to the application’s output directory. When code accessed MSBuild types, the CLR loaded these local assemblies before MSBuildLocator could redirect to the SDK’s assemblies. This created a version mismatch - the NuGet package’s assembly version (15.1.0.0) didn’t match what the SDK expected.
Why .NET 10 Exposed This
On .NET 10,MSBuildLocator.QueryVisualStudioInstances() returns SDK instances filtered by runtime compatibility:
- .NET 8 runtime only sees SDKs ≤ 8.x (e.g., 8.0.415, 7.0.410, 6.0.428)
- .NET 10 runtime sees SDKs ≤ 10.x (e.g., 10.0.100, 9.0.307, 8.0.415)
Version.Major >= 17, thinking these were Visual Studio versions (17.x = VS 2022). But on .NET Core, these are SDK versions (8.x, 9.x, 10.x), so the filter excluded everything, falling back to RegisterDefaults() which then failed because assemblies were already loaded.
The Solution
1. Exclude MSBuild Assemblies from Runtime Output
The key fix is preventing theMicrosoft.Build.* NuGet packages from being copied to the output directory. This is done using ExcludeAssets="runtime":
ExcludeAssets="runtime": Prevents the DLLs from being copied to the output directoryPrivateAssets="all": Prevents these dependencies from flowing to consuming projectsMicrosoft.Build.Locator: Intentionally has NO exclusions - it must be in the output directory
2. Provide Dependencies via .targets File
For NuGet package consumers, a.targets file is included that provides the same package references:
build/EasyAF.MSBuild.targets:
build/ and buildTransitive/ folders of the NuGet package, ensuring it’s imported by direct and transitive consumers.
3. Simplified MSBuild Registration
TheEnsureMSBuildRegistered() method was simplified to explicitly pick the latest available SDK:
4. Early Registration in Applications and Tests
Critical:EnsureMSBuildRegistered() must be called before any code that references Microsoft.Build types is loaded by the CLR.
For applications (like EasyAF.Tools), call it at the start of Main():
[AssemblyInitialize]:
Summary of Changes
| File | Change |
|---|---|
CloudNimble.EasyAF.MSBuild.csproj | Added ExcludeAssets="runtime" PrivateAssets="all" to Microsoft.Build packages |
build/EasyAF.MSBuild.targets | New file providing package references to NuGet consumers |
MSBuildProjectManager.cs | Simplified EnsureMSBuildRegistered() to pick latest SDK |
Application Program.cs | Call EnsureMSBuildRegistered() at start of Main() |
Test AssemblyInitialize.cs | New file with [AssemblyInitialize] calling EnsureMSBuildRegistered() |
For Library Consumers
If you’re consumingEasyAF.MSBuild and need to use MSBuild APIs in your own code:
- Always call
MSBuildProjectManager.EnsureMSBuildRegistered()early - before any code that usesMicrosoft.Buildtypes - For CLI tools: Call it at the start of
Main() - For test projects: Use
[AssemblyInitialize] - For libraries: Document that consumers must register MSBuild before using your library