- name
- building-projects
- description
- Build tool selection and orchestration for .NET projects during modernization upgrades. Use this skill whenever the agent modifies code files, project files, packages, or configuration and needs to validate the build — which is virtually every upgrade task. Covers: choosing between `dotnet build` and `msbuild.exe`, restore strategy, targeted vs solution builds, resolving MSBuild errors (MSB3086, MSB4019, NETSDK1005, etc.), handling multi-targeting scenarios, resource generation failures with .resx files, WPF/WinForms/XAML compilation issues, SDK-style vs legacy csproj differences, and recovering when a build fails. Also use when diagnosing why a project compiles under one tool but not another, or when deciding build scope (single project vs full solution) during iterative migration work.
- metadata
- {"traits":".NET|CSharp|VisualBasic|DotNetCore","discovery":"lazy","autoMatch":"true"}
# Building Projects
## Overview
During .NET modernization, the choice of build tool is not trivial. `dotnet build` and `msbuild.exe`
are not interchangeable — they have different capabilities, different SDK resolution paths, and
different behavior with legacy project features. This skill encodes the expert decision logic so
the agent picks the right tool on the first attempt and recovers intelligently when a build fails.
## Related Skills
| Action | Skill |
|--------|-------|
| Convert legacy project files to SDK-style format | **converting-to-sdk-style** |
| Add/update/remove target frameworks | **managing-target-frameworks** |
| Add/update package references or resolve NuGet version conflicts | **managing-package-references** |
| Modify project properties | **modifying-project-properties** |
Defer to these skills for project format conversion, TFM changes, package management, and property edits.
## Repo-Level Build Skills Take Priority
If the repository contains a custom build skill (e.g., in `.github/skills/`, `.copilot/skills/`,
or a project-level `BUILD.md` with build instructions), defer to that skill for project-specific
build decisions. This skill provides general .NET modernization build logic and should serve as
a fallback when no repo-specific guidance exists.
## IDE Build Tools Take Preference When Available
If the agent is running inside an IDE (Visual Studio, VS Code, Rider) that exposes build-related
tools — such as `build_project`, `build_solution` or similar MCP/Copilot tools —
prefer those over shelling out to the command line. IDE build tools respect the user's local
configuration and surface diagnostics where the user can click-to-navigate.
Fall back to this skill's CLI patterns when the IDE doesn't expose a build tool, the IDE build
tool fails with insufficient error output, the agent needs fine-grained control (targeting a
specific TFM, adjusting verbosity, choosing between `dotnet build` and `msbuild.exe`), or the
agent needs to capture structured build output for automated validation.
## Workflow
Track progress using this checklist:
```
Task Progress:
- [ ] Step 1: Determine the right build tool
- [ ] Step 2: Execute the build
- [ ] Step 3: Diagnose and recover from failures (if any)
- [ ] Step 4: Validate the build output
```
### Step 1: Determine the Right Build Tool
#### Default: Start with `dotnet build`
For SDK-style projects targeting modern .NET only, `dotnet build` is the correct default. It
ships with the .NET SDK, handles NuGet restore implicitly, and works cross-platform.
#### When to switch to `msbuild.exe`
Escalate to the full Visual Studio MSBuild (`msbuild.exe`) when ANY of these conditions apply:
1. **The project has `.resx` files containing images, icons, or binary resources.**
`dotnet build` uses a portable resource generator that cannot process embedded images.
Symptom: `MSB3086`, `MSB3552`, or `LC.exe` errors.
2. **The project is WPF or targets `net*-windows` with XAML.**
WPF's markup compiler requires the full Windows SDK targets that only ship with VS MSBuild.
Symptom: `MC1000`, `XDG0008`, or missing `Microsoft.WinFX.targets`.
3. **The project is WinForms with designer-generated resources.**
The `ResGen.exe` that ships with the .NET SDK may lack support for System.Drawing types
referenced in `.resx` files.
4. **The project multi-targets and includes a `net472` (or any `netXXX` <= `net48`) TFM.**
Building classic .NET Framework TFMs requires reference assemblies and the full MSBuild
toolset. `dotnet build` can handle this IF `Microsoft.NETFramework.ReferenceAssemblies`
is in the project or `Directory.Build.props`, but frequently fails with `NETSDK1005`.
5. **The project uses COM references, VSIX packaging, or T4 templates.**
These rely on VS-specific targets/tasks that the .NET SDK MSBuild doesn't include.
6. **The solution contains C++/CLI (`vcxproj`) projects.**
The .NET CLI cannot build these at all.
7. **Legacy (non-SDK-style) project file that hasn't been converted yet.**
If the project still uses `<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets">`
rather than `<Project Sdk="Microsoft.NET.Sdk">`, only `msbuild.exe` will work. A project
file with a top-level `<Sdk Name="…" />` element or `<Import … Sdk="…" />` instead of the
`Sdk` attribute is SDK-style too, unless it also imports `Microsoft.CSharp.targets` or
`Microsoft.VisualBasic.targets` itself.
#### Build tool decision guide
- **Is the project file SDK-style?**
- **No** → use `msbuild.exe` (full VS). Stop.
- **Yes** → does it target any net4xx TFM?
- **Yes** → is `Microsoft.NETFramework.ReferenceAssemblies` present?
- **Yes** → try `dotnet build` first, fall back to `msbuild.exe` on failure.
- **No** → use `msbuild.exe` (or add the ReferenceAssemblies package first).
- **No** → use `msbuild.exe` if ANY of the following apply, otherwise use `dotnet build`:
- `.resx` with embedded images/icons
- WPF / has XAML pages
- COM references / VSIX / T4 / vcxproj
### Step 2: Execute the Build
#### Always Restore During Migration
During modernization, NuGet packages change constantly — versions get bumped, packages get
swapped, new ones added, old ones removed. A stale `obj/project.assets.json` is one of the
most common causes of phantom build failures.
**Default behavior: always let restore run.**
- For `dotnet build`: restore runs by default — do not add `--no-restore` during migration.
Package references change frequently, and skipping restore causes phantom build failures.
- For `msbuild.exe`: use `msbuild /restore` (or `/r`) to run NuGet restore before building.
This is especially important when already using `msbuild.exe` for the build — mixing
`dotnet restore` as a separate step with `msbuild /build` can cause resolver mismatches.
**When restore can be skipped:** Only when the current migration step modified nothing but
`.cs` files — no `*proj`, `Directory.Build.props`, `Directory.Packages.props`, or
`nuget.config` was touched. If in doubt, restore. The cost is a few seconds; the cost of
a stale restore is a misleading build result.
#### Build Scope: Targeted vs Full Solution
Building the entire solution on every change is too slow for iterative migration work.
Use two tiers:
**Targeted project build (default during iterative work):**
When the agent modifies a project or its files, build only that specific project:
```sh
dotnet build <specific-project.csproj> -r
msbuild.exe <specific-project.csproj> /restore /t:Build /p:Configuration=Release
```
This gives fast feedback (seconds, not minutes) on whether the migration change compiled.
The agent is iterating — it needs signal on its change, not the entire dependency graph.
**Full solution build (final validation only):**
Build the entire solution when:
- The agent is about to mark a task as complete
- A batch of related migration tasks has finished
- The migration step changed something that could ripple across projects (shared interfaces,
public API changes, package updates consumed by multiple projects)
```sh
dotnet build <solution.sln> -r
msbuild.exe <solution.sln> /restore /t:Build /p:Configuration=Release
```
Do NOT default to solution builds for every change. Reserve them as a gate check.
#### Build Patterns
**Simple SDK build (happy path):**
```sh
dotnet build <project.csproj> -r
```
**MSBuild build:**
```sh
msbuild.exe <project.csproj> /restore /t:Build /p:Configuration=Release /v:minimal
```
**Clean build (when incremental build is suspect):**
```sh
dotnet build <project.csproj> --no-incremental
```
Or for MSBuild:
```sh
msbuild.exe <project.csproj> /restore /t:Clean;Build /p:Configuration=Release
```
**Debugging restore failures in isolation:**
When restore itself needs diagnosing, run it separately:
```sh
dotnet restore <project.csproj> --verbosity detailed
```
Then build without restore to isolate the compilation phase:
```sh
dotnet build <project.csproj> --no-restore
```
This is a diagnostic pattern only — not the default workflow.
#### Caching Build Tool Decisions
After determining the right build tool for a project, save the decision to
`scenario-instructions.md` so it doesn't need to be re-derived on every build.
Add or update a `## Build Tool Decisions` section:
```markdown
## Build Tool Decisions
- **MyWebApp.csproj**: msbuild.exe (non-SDK-style, System.Web references)
- **Common.csproj**: dotnet build (SDK-style, no special requirements)
- **Tests.csproj**: dotnet build (SDK-style after conversion)
```
Record the tool, never the `msbuild.exe` path: this file is committed, and the path differs per
machine and changes when Visual Studio updates. Resolve it each session with **Locating MSBuild**.
On subsequent builds, check this section first:
- If the project is listed → use the cached decision
- If not listed or the project changed → re-read this skill and determine the right tool
### Step 3: Diagnose and Recover from Failures
When a build fails, analyze the error codes and take targeted action rather than blindly retrying.
**Load [ref/error-codes.md](ref/error-codes.md)** for the full error catalog
with specific recovery actions per error code.
Key recovery principles:
- Capture full build output (use `-v:detailed` or `/v:diag` on retry for more info)
- Parse for specific error codes, not just "build failed"
- Distinguish between compilation errors (CS\*), MSBuild errors (MSB\*), SDK errors (NETSDK\*),
and NuGet errors (NU\*)
- Report which project within a solution failed (not just "the solution failed")
Do NOT:
- Retry the same command more than once without changing something
- Switch tools without understanding why the first tool failed
- Suppress warnings with `/nowarn` without the user's explicit approval
- Downgrade `TreatWarningsAsErrors` without flagging it
### Step 4: Validate the Build Output
After a successful build during migration, verify:
1. **All warnings fixed** — fix all warnings in the projects being built, not just new ones introduced by this step
2. **Output assembly exists** in the expected `bin/` path
3. **Target framework is correct** — the output folder should match the target TFM (e.g.,
`net10.0/`), not the old TFM
4. **No implicit fallbacks** — if the project was supposed to drop a TFM, confirm it's gone
## Locating MSBuild
When the decision is `msbuild.exe`, the agent must locate it reliably.
### On Windows
> **Write every command on one line, and never assume a shell.** The commands below run in
> the *user's* shell, which is frequently Git Bash or WSL rather than PowerShell or cmd.
> `%VAR%` stays a literal string under bash, a trailing `^` is not a line continuation, and a
> trailing backtick opens command substitution — see **Shell portability** below before
> adapting any of these.
Stop at the first priority that gives a path; each later one is only a fallback.
**Priority 1 — `VSINSTALLDIR` environment variable:**
If the agent is running inside Visual Studio or was launched from a VS Developer Command
Prompt, the `VSINSTALLDIR` environment variable is already set and points to the correct
VS installation. Read it in a way the active shell understands:
| Shell | How to read it |
|---|---|
| cmd | `"%VSINSTALLDIR%\MSBuild\Current\Bin\MSBuild.exe"` |
| PowerShell | `& "$env:VSINSTALLDIR\MSBuild\Current\Bin\MSBuild.exe"` |
| bash | `"$VSINSTALLDIR/MSBuild/Current/Bin/MSBuild.exe"` |
This is the preferred approach — it matches the user's active VS context and avoids
picking a different installation than the one they're working with. If the variable expands
to nothing (it is unset outside a VS context), fall through to Priority 2 rather than
building a path from an empty value. If it is set but that file does not exist, try
`MSBuild\15.0\Bin\MSBuild.exe` under it (Visual Studio 2017), and fall through if neither exists.
**Priority 2 — `validate_dotnet_sdk_installation` output:**
On Windows this tool also reports `Visual Studio MSBuild: <path>` — the instance the upgrade
engine itself loads: the newest with MSBuild that is complete, or registered and needing no
reboot, including Preview, Insiders, Canary and Build Tools. Use that path when Priority 1 gave
none and the output is in your context or you can call the tool with a valid target framework
(an unparseable one returns only a format error). `none found` is not proof that Visual Studio
is missing; continue to Priority 3.
**Priority 3 — `vswhere.exe`:**
Keep it on one line — no `^`, no backtick, no backslash continuations — and pick the form for
your shell. **A quoted path is not a command in PowerShell**: it is just a string expression,
so the next token fails with `Unexpected token '-latest'`. PowerShell needs the call operator `&`:
| Shell | Command |
|---|---|
| PowerShell | `& "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products "*" -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe"` |
| cmd | `"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -products "*" -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe"` |
| Git Bash | `"/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" -latest -prerelease -products "*" -requires Microsoft.Component.MSBuild -find "MSBuild/**/Bin/MSBuild.exe"` |
Under WSL, use `/mnt/c/` where Git Bash uses `/c/`, here and in the web-targets query below,
and convert the Windows path vswhere prints with `wslpath -u` before running it.
Never drop `-prerelease` or `-products "*"`: without them vswhere skips Preview, Insiders,
Canary and Build Tools, and prints nothing on a machine that has only those. Keep `"*"` quoted —
unquoted, bash expands it to file names, and vswhere prints nothing and exits 0.
The same `&` rule applies to any quoted executable path you invoke from PowerShell, including
the `powershell -File` form used elsewhere in these skills.
**Priority 4 — install folders:**
List the `<version>\<edition>` folders under `C:\Program Files\Microsoft Visual Studio` and
`C:\Program Files (x86)\Microsoft Visual Studio` (editions include Preview, Insiders, Canary
and BuildTools) and use one that contains `MSBuild\Current\Bin\MSBuild.exe` (Visual Studio
2017: `MSBuild\15.0\Bin\MSBuild.exe`).
**Legacy web projects need the web targets too.** A project that imports
`Microsoft.WebApplication.targets` builds only with an instance that has
`MSBuild\Microsoft\VisualStudio\v*\WebApplications\Microsoft.WebApplication.targets`, and the
newest instance may not (Priority 2 reports whether it does). If the chosen instance lacks it,
list the instances that have it, newest first, and use `MSBuild\Current\Bin\MSBuild.exe`
(2017: `MSBuild\15.0\Bin\MSBuild.exe`) under the first result's instance folder (the part before
`\MSBuild\`):
| Shell | Command |
|---|---|
| PowerShell | `& "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -sort -prerelease -products "*" -requires Microsoft.Component.MSBuild -find "MSBuild\Microsoft\VisualStudio\v*\WebApplications\Microsoft.WebApplication.targets"` |
| cmd | `"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -sort -prerelease -products "*" -requires Microsoft.Component.MSBuild -find "MSBuild\Microsoft\VisualStudio\v*\WebApplications\Microsoft.WebApplication.targets"` |
| Git Bash | `"/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" -sort -prerelease -products "*" -requires Microsoft.Component.MSBuild -find "MSBuild/Microsoft/VisualStudio/v*/WebApplications/Microsoft.WebApplication.targets"` |
If it prints nothing, MSBuild is installed but no instance has the web targets. Stop retrying and
tell the user to add the **ASP.NET and web development** workload (Build Tools: **Web development
build tools**) to the instance you found.
**If every priority comes up empty,** report that no Visual Studio MSBuild was found and list
what you checked. Do not substitute `dotnet build` for a project the decision guide sent to
`msbuild.exe`: the SDK has no Visual Studio targets, so it fails with `MSB4019` and hides the
real cause.
### Shell portability (read before writing any command)
The `execute` tool runs in the shell the **user** configured, and nothing detects which one
it is. A command written for one shell fails — sometimes silently — in another.
| Construct | cmd | PowerShell | bash (Git Bash / WSL) |
|---|---|---|---|
| `%VAR%` | expands | literal | **literal** |
| `$env:VAR` | literal | expands | literal |
| `$VAR` | literal | **expands to a PowerShell variable — empty unless you set one** (env vars need `$env:VAR`) | expands |
| `^` at end of line | continuation | literal | **not a continuation — command fragments** |
GitHubで見る