Claude
Skills
Sign in
Back

dotnet-build-optimization

Included with Lifetime
$97 forever

Diagnosing slow builds or incremental failures. Binary logs, parallel builds, restore.

General

What this skill does


# dotnet-build-optimization

Guidance for diagnosing and fixing build performance problems: incremental build failure diagnosis workflows, binary log analysis with MSBuild Structured Log Viewer, parallel build configuration, build caching, and restore optimization. Covers the diagnostic workflow from symptom (full rebuild on every build) through root cause (missing Inputs/Outputs, timestamp corruption, generator side effects) to fix.

**Version assumptions:** .NET 8.0+ SDK (MSBuild 17.8+). All examples use SDK-style projects.

**Scope boundary:** This skill owns build optimization and diagnostics -- incremental build failures, binary logs, parallel builds, build caching, and restore optimization. MSBuild error interpretation and CI drift diagnosis is owned by [skill:dotnet-build-analysis]. MSBuild authoring (targets, props, items, conditions) is owned by [skill:dotnet-msbuild-authoring]. Custom task development is owned by [skill:dotnet-msbuild-tasks]. NuGet lock files and Central Package Management configuration is owned by [skill:dotnet-project-structure].

Cross-references: [skill:dotnet-msbuild-authoring] for custom targets, import ordering, and incremental build authoring patterns. [skill:dotnet-msbuild-tasks] for custom task development. [skill:dotnet-build-analysis] for interpreting MSBuild errors, NuGet restore failures, and CI drift diagnosis. [skill:dotnet-project-structure] for lock files, CPM, and nuget.config configuration.

---

## Incremental Build Failure Diagnosis

When a target runs on every build despite no source changes, the build is not incremental. This wastes time and masks real changes. The diagnosis workflow follows a repeatable pattern: detect the symptom, capture a binary log, identify the offending target, determine why incrementality failed, and apply the fix.

### Diagnosis Workflow

```
1. Symptom: Build takes longer than expected, or output says
   "Building target 'X' completely" on every build
2. Capture binary log:  dotnet build /bl
3. Open the .binlog in MSBuild Structured Log Viewer
4. Search for targets that ran (not skipped)
5. Check: Does the target have Inputs/Outputs?
   - No  -> Add Inputs/Outputs (see fix patterns below)
   - Yes -> Compare timestamps: are outputs older than inputs?
           -> Check for volatile writers or missing output files
6. Apply fix, rebuild, verify target is skipped
```

### Step 1: Capture a Binary Log

```bash
# Produce msbuild.binlog in the project directory
dotnet build /bl

# Named log file
dotnet build /bl:build-debug.binlog

# Binary log for restore + build (captures full pipeline)
dotnet build /bl -restore
```

The `/bl` switch records every MSBuild event -- property evaluations, item lists, target entry/exit, task execution, and timestamps -- into a compact binary format. Binary logs contain full source paths and environment variables; do not commit them to version control or share publicly.

### Step 2: Open in MSBuild Structured Log Viewer

Download from [msbuildlog.com](https://msbuildlog.com/). Open the `.binlog` file. Key views:

| View | Use |
|---|---|
| **Timeline** | See which targets ran in parallel and how long each took |
| **Target Results** | Filter by "Built" (ran) vs "Skipped" (incremental hit) |
| **Search** | Find specific target names, property values, or file paths |
| **Properties** | Inspect evaluated property values at any point in the build |
| **Items** | Inspect item collections (Compile, Content, etc.) with metadata |

### Step 3: Find the Non-Incremental Target

In the Structured Log Viewer, search for the target name and check its result. A target that should be incremental but ran fully will show "Building target 'X' completely" with a reason:

- **"Output file does not exist"** -- an expected output file is missing or was deleted
- **"Input file is newer than output file"** -- a source file changed, or a preceding step rewrote an output
- **No Inputs/Outputs declared** -- the target always runs because MSBuild has no way to check freshness

---

## Common Incremental Build Failure Patterns

### Missing Inputs/Outputs on Custom Targets

**Symptom:** Custom target runs on every build.

**Root cause:** The target has no `Inputs`/`Outputs` attributes. Without them, MSBuild runs the target unconditionally.

**Fix:** Add `Inputs` and `Outputs` that reflect the actual files read and written:

```xml
<!-- BEFORE: runs every build -->
<Target Name="GenerateVersionFile" BeforeTargets="CoreCompile">
  <WriteLinesToFile File="$(IntermediateOutputPath)Version.g.cs"
                    Lines="[assembly: System.Reflection.AssemblyInformationalVersion(&quot;$(Version)&quot;)]"
                    Overwrite="true" />
</Target>

<!-- AFTER: only runs when Version property changes (via project file edit) -->
<Target Name="GenerateVersionFile"
        BeforeTargets="CoreCompile"
        Inputs="$(MSBuildProjectFullPath)"
        Outputs="$(IntermediateOutputPath)Version.g.cs">
  <WriteLinesToFile File="$(IntermediateOutputPath)Version.g.cs"
                    Lines="[assembly: System.Reflection.AssemblyInformationalVersion(&quot;$(Version)&quot;)]"
                    Overwrite="true" />
</Target>
```

See [skill:dotnet-msbuild-authoring] for full Inputs/Outputs patterns and batching.

### File Copy Timestamp Corruption

**Symptom:** Target re-runs because output file timestamps are always newer than inputs.

**Root cause:** A `Copy` task without `SkipUnchangedFiles="true"` updates the destination timestamp on every copy, even when content is identical.

**Fix:**

```xml
<!-- BEFORE: copies every build, resetting timestamps -->
<Copy SourceFiles="@(ConfigTemplate)"
      DestinationFolder="$(OutputPath)" />

<!-- AFTER: skips unchanged files, preserving timestamps -->
<Copy SourceFiles="@(ConfigTemplate)"
      DestinationFolder="$(OutputPath)"
      SkipUnchangedFiles="true" />
```

### Generators Writing Unconditionally

**Symptom:** A code generator target runs every build even though inputs have not changed.

**Root cause:** The generator writes output files unconditionally, updating their timestamps even when content is identical. The next build sees "input newer than output" (because the generator itself is an input to downstream targets).

**Fix:** Write to a temp file first, then copy only if content differs:

```xml
<Target Name="GenerateCode"
        BeforeTargets="CoreCompile"
        Inputs="@(SchemaFile)"
        Outputs="@(SchemaFile->'$(IntermediateOutputPath)%(Filename).g.cs')">
  <!-- Write to temp file -->
  <Exec Command="codegen %(SchemaFile.Identity) -o $(IntermediateOutputPath)%(SchemaFile.Filename).g.cs.tmp" />

  <!-- Copy only if content changed (preserves timestamp when unchanged) -->
  <Copy SourceFiles="$(IntermediateOutputPath)%(SchemaFile.Filename).g.cs.tmp"
        DestinationFiles="$(IntermediateOutputPath)%(SchemaFile.Filename).g.cs"
        SkipUnchangedFiles="true" />
</Target>
```

### Volatile Intermediate Files

**Symptom:** A target that depends on intermediate outputs re-runs because an earlier target always regenerates those files.

**Root cause:** An upstream target produces intermediate files (e.g., generated code, resource bundles) without proper Inputs/Outputs, causing those files to be rewritten every build. Downstream targets see them as "changed" and re-run.

**Fix:** Add Inputs/Outputs to the upstream target. If the upstream target is from the SDK or a NuGet package and cannot be modified, use `Touch` task to reset timestamps on its outputs to a stable value when content has not changed.

---

## Binary Log Analysis

### Capturing Binary Logs

```bash
# Basic binary log (outputs msbuild.binlog)
dotnet build /bl

# Named output file
dotnet build /bl:diagnostic.binlog

# Include restore phase
dotnet build /bl -restore

# Detailed verbosity in console + binary log
dotnet build /bl /v:minimal
```

Binary logs capture everything regardless of the `/v:` verbosity level. The `/v:` switch only controls console outp

Related in General