Clean up dotnet-msbuild: unify lock files, compact skill, add .gitattributes (#116)

* Clean up dotnet-msbuild: unify lock files, compact skill, add .gitattributes

- Add .gitattributes with * text=auto eol=lf for consistent line endings
- Unify compiled knowledge lock file names across copilot-extension and
  agentic-workflows (build-errors, performance, style-and-modernization)
- Deduplicate KnowledgeMap in build.ps1 into shared KnowledgeGroups
- Compact directory-build-organization skill (16K -> 8K chars) so it fits
  within compiled bundle limits; move detailed examples to references/
- Remove broken links to non-existent docs/copilot-extension-design.md
- Regenerate all lock files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add repo-level AGENTS.md with component build instructions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Inline references/ content during knowledge compilation

Update Read-Skill in build.ps1 to resolve [text](references/*.md) links
and replace them with the referenced file content. This ensures compiled
lock files include the full reference material alongside the SKILL.md
content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update agentic workflows to leverage compiled knowledge

- build-failure-analysis: add binlog-mcp tool usage, bin/obj clash and
  generated file checks, common error categories
- build-perf-audit: add bottleneck classification, concrete optimization
  recommendations (MSBuild Server, ArtifactsPath, graph build), incremental
  build health check
- msbuild-pr-review: reference AP codes from anti-pattern catalog, add
  Central Package Management and Directory.Build centralization checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Change agentic workflow triggers to comment-based invocation

- build-failure-analysis: /analyze-build-failure
- build-perf-audit: /audit-build-perf
- msbuild-pr-review: /review-msbuild

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Viktor Hofer
2026-02-25 10:33:36 +01:00
committed by GitHub
parent aef6dd634f
commit c7c74daf8e
21 changed files with 1631 additions and 2169 deletions
+2
View File
@@ -0,0 +1,2 @@
# Normalize all text files to LF line endings
* text=auto eol=lf
+19
View File
@@ -0,0 +1,19 @@
# Repository Instructions
This repository contains skill components under `src/`. Each subdirectory in `src/` is an independent component (e.g., `src/dotnet-msbuild`, `src/dotnet`).
## Build
When you modify files in a component, check whether that component has a `build.ps1` file in its root directory. If it does, run it after making changes to validate and regenerate any compiled artifacts.
```powershell
pwsh src/<component>/build.ps1
```
**Example:** After editing skills in `src/dotnet-msbuild/`, run:
```powershell
pwsh src/dotnet-msbuild/build.ps1
```
This validates skill frontmatter and recompiles knowledge lock files. Always commit the regenerated lock files together with your changes.
+2 -2
View File
@@ -10,6 +10,8 @@ Comprehensive MSBuild and .NET build skills: failure diagnosis, performance opti
|-------|-------------|
| [`binlog-failure-analysis`](skills/binlog-failure-analysis/) | Binary log analysis for deep build failure diagnosis |
| [`binlog-generation`](skills/binlog-generation/) | Binary log generation conventions |
| [`check-bin-obj-clash`](skills/check-bin-obj-clash/) | Output path conflict detection for multi-targeting and multi-project builds |
| [`including-generated-files`](skills/including-generated-files/) | Including build-generated files in MSBuild's build process |
### Build Performance Optimization
@@ -28,8 +30,6 @@ Comprehensive MSBuild and .NET build skills: failure diagnosis, performance opti
| [`msbuild-antipatterns`](skills/msbuild-antipatterns/) | Anti-pattern catalog with detection rules, severity, and BAD→GOOD fixes |
| [`msbuild-modernization`](skills/msbuild-modernization/) | Legacy to SDK-style project migration with before/after examples |
| [`directory-build-organization`](skills/directory-build-organization/) | Directory.Build.props/targets/rsp organization and central package management |
| [`check-bin-obj-clash`](skills/check-bin-obj-clash/) | Output path conflict detection for multi-targeting and multi-project builds |
| [`including-generated-files`](skills/including-generated-files/) | Including build-generated files in MSBuild's build process |
## 🤖 Agents
@@ -1,8 +1,8 @@
---
on:
workflow_run:
workflows: ["CI", "Build", "CI Build"]
types: [completed]
issue_comment:
types: [created]
body: "/analyze-build-failure"
permissions:
contents: read
@@ -12,7 +12,7 @@ permissions:
imports:
- shared/binlog-mcp.md
- shared/compiled/build-failure-knowledge.lock.md
- shared/compiled/build-errors.lock.md
tools:
github:
@@ -32,15 +32,24 @@ You are an MSBuild build failure analysis agent. When a CI build workflow comple
1. **Check if the triggering workflow failed**: Use the GitHub tools to check the workflow run status. If it succeeded, exit without action.
2. **Get failure details**:
2. **Get failure details**:
- Get the failed workflow run details and job logs
- Identify which jobs and steps failed
- Look for .NET build error patterns (CS, MSB, NU, NETSDK, FS, BC error codes)
- Look for .NET build error patterns (CS, MSB, NU, NETSDK, FS, BC, AD error codes)
3. **Analyze the failure**:
- If binlog files are available as artifacts, download and analyze them with binlog-mcp tools
- If binlog files are available as artifacts, download and analyze them with binlog-mcp tools:
1. `load_binlog` to load the binary log
2. `get_diagnostics` for errors and warnings
3. `search_binlog` for specific patterns (see query language in imported knowledge)
- Otherwise, analyze the build output logs for error patterns
- Use MSBuild knowledge to identify root causes
- Check for common failure categories:
- **Compile errors** (CS prefix): missing types, syntax errors, nullable violations
- **MSBuild errors** (MSB prefix): target failures, import issues, property evaluation
- **NuGet errors** (NU prefix): restore failures, version conflicts, missing packages
- **SDK errors** (NETSDK prefix): SDK not found, workload issues, TFM problems
- **Bin/obj clashes**: multiple projects or TFMs writing to the same output directory — use `search_binlog` for file access errors or MSB3277 warnings
- **Generated file issues**: source generators failing or generated files not included in compilation (CS8785, AD0001)
4. **Post findings**:
- If the failure is associated with a pull request, post a comment on the PR
@@ -51,6 +60,7 @@ You are an MSBuild build failure analysis agent. When a CI build workflow comple
## Guidelines
- Only post comments for genuine build failures, not infrastructure issues
- Be specific: reference exact error codes, file paths, and line numbers when available
- Suggest concrete fixes, not vague advice
- Suggest concrete fixes, not vague advice — show corrected XML or commands
- If binlogs are available, always prefer binlog analysis over parsing console output
- If you can't determine the cause, say so rather than guessing
- Don't repeat the entire build log — summarize the key errors
@@ -1,6 +1,8 @@
---
on:
schedule: weekly
issue_comment:
types: [created]
body: "/audit-build-perf"
permissions:
contents: read
@@ -9,7 +11,7 @@ permissions:
imports:
- shared/binlog-mcp.md
- shared/compiled/perf-audit-knowledge.lock.md
- shared/compiled/performance.lock.md
tools:
github:
@@ -30,32 +32,45 @@ You are a build performance auditing agent. Each week, you analyze the repositor
1. **Build with binlog**: Run `dotnet build /bl:perf-audit.binlog -m` to generate a performance baseline
2. **Analyze performance**:
2. **Analyze performance** using binlog-mcp tools:
- Load the binlog with `load_binlog`
- Get total build duration
- Run `get_node_timeline` for parallelism analysis
- Run `get_expensive_projects(top_number=10, sortByExclusive=true)`
- Run `get_expensive_targets(top_number=10)`
- Run `get_expensive_tasks(top_number=10)`
- Run `get_expensive_analyzers(top_number=5)`
- `get_node_timeline` → assess parallelism utilization across build nodes
- `get_expensive_projects(top_number=10, sortByExclusive=true)` → find time-heavy projects
- `get_expensive_targets(top_number=15)` → find dominant targets (Csc, RAR, Copy)
- `get_expensive_tasks(top_number=15)` → find dominant tasks
- `get_expensive_analyzers(top_number=10)` → check Roslyn analyzer overhead
3. **Track trends**: Use `cache-memory` to store and compare:
3. **Classify bottlenecks** into categories:
- **Serialization**: nodes idle, one project blocking others → project graph issue
- **Compilation**: Csc task dominant → too much code in one project, or expensive analyzers
- **Resolution**: ResolveAssemblyReference dominant → too many references
- **I/O**: Copy/Move tasks dominant → excessive file copying, consider hardlinks
- **Evaluation**: slow startup before compilation → expensive glob patterns or deep import chains
- **Analyzers**: disproportionate analyzer time → specific analyzer is expensive
4. **Track trends**: Use `cache-memory` to store and compare:
- Total build duration
- Top 5 most expensive projects and their times
- Analyzer overhead percentage
- Node utilization percentage
4. **Generate report**: Create an issue with:
5. **Generate report**: Create an issue with:
- **Summary**: Total build time, comparison to previous week
- **Top bottlenecks**: Most expensive projects/targets/tasks
- **Top bottlenecks**: Most expensive projects/targets/tasks with durations
- **Trends**: Is build time improving or degrading?
- **Recommendations**: Actionable suggestions for improvement
- **Analyzer impact**: If analyzer time is >30% of compilation, flag it
- **Recommendations** prioritized by effort:
- *Quick wins*: `/maxcpucount`, `RunAnalyzers=false` in dev, MSBuild Server (`DOTNET_CLI_USE_MSBUILD_SERVER=1`)
- *Medium effort*: `ArtifactsPath` for bin/obj separation, incremental build fixes (missing Inputs/Outputs on custom targets), disable expensive analyzers in CI
- *Large effort*: graph build (`/graph`), project splitting, dependency graph trimming
- **Analyzer impact**: If analyzer time is >30% of compilation, flag specific analyzers
- **Incremental build health**: Check if no-op builds are truly fast (should be <5% of clean build)
5. **Only create issue if noteworthy**: Don't create an issue if build times are stable and within acceptable range. Only report when:
6. **Only create issue if noteworthy**: Don't create an issue if build times are stable and within acceptable range. Only report when:
- Build time increased >10% from previous audit
- A new bottleneck appeared in top 5
- Node utilization dropped below 70%
- Incremental builds are broken (no-op build > 10% of clean build time)
- It's the first audit (establish baseline)
## Guidelines
@@ -1,24 +1,15 @@
---
on:
pull_request:
types: [opened, synchronize]
paths:
- "**/*.csproj"
- "**/*.vbproj"
- "**/*.fsproj"
- "**/*.props"
- "**/*.targets"
- "**/Directory.Build.*"
- "**/Directory.Packages.props"
- "**/nuget.config"
- "**/global.json"
issue_comment:
types: [created]
body: "/review-msbuild"
permissions:
contents: read
pull-requests: read
imports:
- shared/compiled/pr-review-knowledge.lock.md
- shared/compiled/style-and-modernization.lock.md
tools:
github:
@@ -38,38 +29,41 @@ You are a specialized reviewer for MSBuild project file changes. When a PR modif
1. **Get the PR diff**: Retrieve the changed files and their diffs
2. **Filter to MSBuild files**: Focus only on .csproj, .vbproj, .fsproj, .props, .targets, Directory.Build.*, Directory.Packages.props, nuget.config, global.json
3. **Analyze each changed file** against these criteria:
3. **Analyze each changed file** against the anti-pattern catalog and modernization guide in the imported knowledge:
### Check for Anti-patterns
- Hardcoded absolute paths (should use MSBuild properties)
- Explicit file includes that SDK handles automatically
- `<Reference>` tags with HintPath that should be `<PackageReference>` (note: `<Reference>` is valid for .NET Framework GAC assemblies)
- Missing `Condition` quotes: must be `'$(Prop)' == 'value'`
- Properties conditioned on `$(TargetFramework)` in `.props` files (silently fails for single-targeting projects — move to `.targets`)
- Missing `PrivateAssets="all"` on analyzer/tool packages
- Properties that belong in Directory.Build.props (if duplicated)
### Check for Anti-patterns (AP codes from imported knowledge)
- **AP-01** Hardcoded absolute paths (should use `$(MSBuildThisFileDirectory)` or similar)
- **AP-02** Explicit file includes that SDK handles automatically (`<Compile Include="**/*.cs" />`)
- **AP-05** `<Reference>` with HintPath that should be `<PackageReference>` (note: `<Reference>` is valid for .NET Framework GAC assemblies)
- **AP-06** Missing `Condition` quotes: must be `'$(Prop)' == 'value'`
- **AP-08** Missing `PrivateAssets="all"` on analyzer/tool packages
- **AP-10** Custom targets missing `Inputs`/`Outputs` (breaks incremental builds)
- **AP-12** Properties that belong in Directory.Build.props (if duplicated across projects)
- **AP-17** Side effects during property evaluation (file writes, network calls)
- **AP-18** Platform-specific `<Exec>` without OS condition guard
- **AP-21** Properties conditioned on `$(TargetFramework)` in `.props` files (silently fails for single-targeting projects — move to `.targets`). **Item and target conditions are NOT affected** and must not be flagged.
### Check for Correctness
- Custom targets missing `Inputs`/`Outputs` (breaks incremental builds)
- Potential bin/obj path clashes in multi-targeting
- Package version conflicts
- Incorrect TFM syntax
- Side effects during property evaluation (file writes, network calls)
- Platform-specific `<Exec>` without OS condition guard
- Condition logic that is always true/false
### Check for Modernization Opportunities
- Legacy project format that could be SDK-style
- `packages.config` that should be PackageReference
- Properties that could use Central Package Management
- Properties that could use Central Package Management (`Directory.Packages.props`)
- Duplicated settings that should be centralized in `Directory.Build.props`
4. **Post review**: Comment on the PR with findings organized by severity:
- 🔴 Issues that should be fixed before merge
- 🟡 Suggestions for improvement
- 🔴 Issues that should be fixed before merge (broken builds, correctness issues)
- 🟡 Suggestions for improvement (anti-patterns, modernization)
- 🟢 Positive patterns observed
## Guidelines
- Only comment on MSBuild-specific issues, not general code quality
- Reference AP codes when flagging anti-patterns (e.g., "AP-08: Missing PrivateAssets")
- Be constructive and explain WHY something is an issue
- Provide the correct code when suggesting a fix
- Provide the corrected XML when suggesting a fix — show BAD → GOOD
- Don't comment if the changes look good — only post when there are actionable findings
- Keep comments concise and focused
@@ -1,4 +1,4 @@
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
<!-- AUTO-GENERATED — DO NOT EDIT -->
# Analyzing MSBuild Failures with Binary Logs
@@ -285,6 +285,97 @@ When binlog analysis reveals these patterns, here's the fast path:
---
# Generate Binary Logs
**Pass the `/bl` switch when running any MSBuild-based command.** This is a non-negotiable requirement for all .NET builds.
## Commands That Require /bl
You MUST add the `/bl:{}` flag to:
- `dotnet build`
- `dotnet test`
- `dotnet pack`
- `dotnet publish`
- `dotnet restore`
- `msbuild` or `msbuild.exe`
- Any other command that invokes MSBuild
## Preferred: Use `{}` for Automatic Unique Names
> **Note:** The `{}` placeholder requires MSBuild 17.8+ / .NET 8 SDK or later.
The `{}` placeholder in the binlog filename is replaced by MSBuild with a unique identifier, guaranteeing no two builds ever overwrite each other — without needing to track or check existing files.
```bash
# Every invocation produces a distinct file automatically
dotnet build /bl:{}
dotnet test /bl:{}
dotnet build --configuration Release /bl:{}
```
**PowerShell requires escaping the braces:**
```powershell
# PowerShell: escape { } as {{ }}
dotnet build -bl:{{}}
dotnet test -bl:{{}}
```
## Why This Matters
1. **Unique names prevent overwrites** - You can always go back and analyze previous builds
2. **Failure analysis** - When a build fails, the binlog is already there for immediate analysis
3. **Comparison** - You can compare builds before and after changes
4. **No re-running builds** - You never need to re-run a failed build just to generate a binlog
## Examples
```bash
# ✅ CORRECT - {} generates a unique name automatically (bash/cmd)
dotnet build /bl:{}
dotnet test /bl:{}
# ✅ CORRECT - PowerShell escaping
dotnet build -bl:{{}}
dotnet test -bl:{{}}
# ❌ WRONG - Missing /bl flag entirely
dotnet build
dotnet test
# ❌ WRONG - No filename (overwrites the same msbuild.binlog every time)
dotnet build /bl
dotnet build /bl
```
## When a Specific Filename Is Required
If the binlog filename needs to be known upfront (e.g., for CI artifact upload), or if `{}` is not available in the installed MSBuild version, pick a name that won't collide with existing files:
1. Check for existing `*.binlog` files in the directory
2. Choose a name not already taken (e.g., by incrementing a counter from the highest existing number)
```bash
# Example: directory contains 3.binlog — use 4.binlog
dotnet build /bl:4.binlog
```
## Cleaning the Repository
When cleaning the repository with `git clean`, **always exclude binlog files** to preserve your build history:
```bash
# ✅ CORRECT - Exclude binlog files from cleaning
git clean -fdx -e "*.binlog"
# ❌ WRONG - This deletes binlog files (they're usually in .gitignore)
git clean -fdx
```
This is especially important when iterating on build fixes - you need the binlogs to analyze what changed between builds.
---
# Detecting OutputPath and IntermediateOutputPath Clashes
## Overview
@@ -620,4 +711,136 @@ When multiple evaluations share an output path, compare these global properties
## Testing Fixes
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
---
# Including Generated Files Into Your Build
## Overview
Files generated during the build are generally ignored by the build process. This leads to confusing results such as:
- Generated files not being included in the output directory
- Generated source files not being compiled
- Globs not capturing files created during the build
This happens because of how MSBuild's build phases work.
## Quick Takeaway
For code files generated during the build - we need to add those to `Compile` and `FileWrites` item groups within the target generating the file(s):
```xml
<ItemGroup>
<Compile Include="$(GeneratedFilePath)" />
<FileWrites Include="$(GeneratedFilePath)" />
</ItemGroup>
```
The target generating the file(s) should be hooked before CoreCompile and BeforeCompile targets - `BeforeTargets="CoreCompile;BeforeCompile"`
## Why Generated Files Are Ignored
For detailed explanation, see [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview).
### Evaluation Phase
MSBuild reads your project, imports everything, creates Properties, expands globs for Items **outside of Targets**, and sets up the build process.
### Execution Phase
MSBuild runs Targets & Tasks with the provided Properties & Items to perform the build.
**Key Takeaway:** Files generated during execution don't exist during evaluation, therefore they aren't found. This particularly affects files that are globbed by default, such as source files (`.cs`).
## Solution: Manually Add Generated Files
When files are generated during the build, manually add them into the build process. The approach depends on the type of file being generated.
### Use `$(IntermediateOutputPath)` for Generated File Location
Always use `$(IntermediateOutputPath)` as the base directory for generated files. **Do not** hardcode `obj\` or construct the intermediary path manually (e.g., `obj\$(Configuration)\$(TargetFramework)\`). The intermediate output path can be redirected to a different location in some build configurations (e.g., shared output directories, CI environments). Using `$(IntermediateOutputPath)` ensures your target works correctly regardless of the actual path.
### Always Add Generated Files to `FileWrites`
Every generated file should be added to the `FileWrites` item group. This ensures that MSBuild's `Clean` target properly removes your generated files. Without this, generated files will accumulate as stale artifacts across builds.
```xml
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
</ItemGroup>
```
### Basic Pattern (Non-Code Files)
For generated files that need to be copied to output (config files, data files, etc.), add them to `Content` or `None` items before `BeforeBuild`:
```xml
<Target Name="IncludeGeneratedFiles" BeforeTargets="BeforeBuild">
<!-- Your logic that generates files goes here -->
<ItemGroup>
<None Include="$(IntermediateOutputPath)my-generated-file.xyz" CopyToOutputDirectory="PreserveNewest"/>
<!-- Capture all files of a certain type with a glob -->
<None Include="$(IntermediateOutputPath)generated\*.xyz" CopyToOutputDirectory="PreserveNewest"/>
<!-- Register generated files for proper cleanup -->
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
<FileWrites Include="$(IntermediateOutputPath)generated\*.xyz" />
</ItemGroup>
</Target>
```
### For Generated Source Files (Code That Needs Compilation)
If you're generating `.cs` files that need to be compiled, use **`BeforeTargets="CoreCompile;BeforeCompile"`**. This is the correct timing for adding `Compile` items — it runs late enough that the file generation has occurred, but before the compiler runs. Using `BeforeBuild` is too early for some scenarios and may not work reliably with all SDK features.
```xml
<Target Name="IncludeGeneratedSourceFiles" BeforeTargets="CoreCompile;BeforeCompile">
<PropertyGroup>
<GeneratedCodeDir>$(IntermediateOutputPath)Generated\</GeneratedCodeDir>
<GeneratedFilePath>$(GeneratedCodeDir)MyGeneratedFile.cs</GeneratedFilePath>
</PropertyGroup>
<MakeDir Directories="$(GeneratedCodeDir)" />
<!-- Your logic that generates the .cs file goes here -->
<ItemGroup>
<Compile Include="$(GeneratedFilePath)" />
<FileWrites Include="$(GeneratedFilePath)" />
</ItemGroup>
</Target>
```
Note: Specifying both `CoreCompile` and `BeforeCompile` ensures the target runs before whichever target comes first, providing robust ordering regardless of customizations in the build.
## Target Timing
Choose the `BeforeTargets` value based on the type of file being generated:
- **`BeforeTargets="BeforeBuild"`** — For non-code files added to `None` or `Content`. Runs early enough for copy-to-output scenarios.
- **`BeforeTargets="CoreCompile;BeforeCompile"`** — For generated source files added to `Compile`. Ensures the file is included before the compiler runs.
- **`BeforeTargets="AssignTargetPaths"`** — The "final stop" before `None` and `Content` items (among others) are transformed into new items. Use as a fallback if `BeforeBuild` is too early.
## Globbing Behavior
Globs behave according to **when** the glob took place:
| Glob Location | Files Captured |
|---------------|----------------|
| Outside of a target | Only files visible during Evaluation phase (before build starts) |
| Inside of a target | Files visible when the target runs (can capture generated files if timed correctly) |
This is why the solution places the `<ItemGroup>` inside a `<Target>` - the glob runs during execution when the generated files exist.
## Relevant Links
- [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview)
- [Evaluation Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#evaluation-phase)
- [Execution Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#execution-phase)
- [Common Item Types](https://docs.microsoft.com/visualstudio/msbuild/common-msbuild-project-items)
- [How the SDK imports items by default](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.props)
- [Official docs: Handle generated files](https://learn.microsoft.com/visualstudio/msbuild/customize-your-build#handle-generated-files)
@@ -1,4 +1,4 @@
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
<!-- AUTO-GENERATED — DO NOT EDIT -->
# Build Performance Baseline & Optimization
@@ -1,4 +1,4 @@
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
<!-- AUTO-GENERATED — DO NOT EDIT -->
# MSBuild Anti-Pattern Catalog
@@ -181,7 +181,28 @@ Use this catalog when scanning project files for improvements.
**Why it's bad**: Without `PrivateAssets="all"`, analyzer and build-tool packages flow as transitive dependencies to consumers of your library. Consumers get unwanted analyzers or build-time tools they didn't ask for.
See [`references/private-assets.md`](references/private-assets.md) for BAD/GOOD examples and the full list of packages that need this.
See # PrivateAssets for Analyzers and Build Tools
Analyzer and build-tool packages should always use `PrivateAssets="all"` to prevent them from flowing as transitive dependencies to consumers of your library.
```xml
<!-- BAD: Flows to consumers -->
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageReference Include="MinVer" Version="5.0.0" />
<!-- GOOD: Stays private -->
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="MinVer" Version="5.0.0" PrivateAssets="all" />
```
**Packages that almost always need `PrivateAssets="all"`:**
- Roslyn analyzers (`*.Analyzers`, `*.CodeFixes`)
- Source generators
- SourceLink packages (`Microsoft.SourceLink.*`)
- Versioning tools (`MinVer`, `Nerdbank.GitVersioning`)
- Build-only tools (`Microsoft.DotNet.ApiCompat`, etc.) for BAD/GOOD examples and the full list of packages that need this.
---
@@ -276,7 +297,36 @@ See `directory-build-organization` skill for full guidance on structuring `Direc
**Why it's bad**: The target runs on every build, even when nothing changed. This defeats incremental build and slows down no-op builds.
See [`references/incremental-build-inputs-outputs.md`](references/incremental-build-inputs-outputs.md) for BAD/GOOD examples and the full pattern including FileWrites registration.
See # Incremental Build: Inputs and Outputs on Custom Targets
Custom targets **must** specify `Inputs` and `Outputs` attributes so MSBuild can skip them when up-to-date. Without both attributes, the target runs on every build.
```xml
<!-- BAD: Runs every time -->
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile">
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
Lines="// Generated at $(Version)" Overwrite="true" />
</Target>
<!-- GOOD: Skipped when up-to-date -->
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile"
Inputs="$(MSBuildProjectFile)" Outputs="$(IntermediateOutputPath)BuildInfo.g.cs">
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
Lines="// Generated at $(Version)" Overwrite="true" />
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
<Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
</ItemGroup>
</Target>
```
**Key points:**
- **`Inputs`** should include `$(MSBuildProjectFile)` plus any source files that drive generation
- **`Outputs`** should use `$(IntermediateOutputPath)` so generated files go in `obj/` and are managed by MSBuild
- **`FileWrites`** registration ensures `dotnet clean` removes the generated file
- **`Compile` inclusion** adds the generated file to compilation without requiring it at evaluation time
See the `incremental-build` skill for deep guidance on diagnosing broken incremental builds, FileWrites tracking, and Visual Studio's Fast Up-to-Date Check. for BAD/GOOD examples and the full pattern including FileWrites registration.
See `incremental-build` skill for deep guidance on Inputs/Outputs, FileWrites, and up-to-date checks.
@@ -584,6 +634,8 @@ When reviewing an MSBuild file, scan for these in order:
---
## msbuild-modernization
# MSBuild Modernization: Legacy to SDK-style Migration
## Identifying Legacy vs SDK-style Projects
@@ -1008,75 +1060,6 @@ After migration, consider enabling modern C# features:
Centralizes NuGet version management across a multi-project solution. See [https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) for details.
**Step 1:** Create `Directory.Packages.props` at the repository root with `<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>` and `<PackageVersion>` items for all packages.
**Step 1:** Create `Directory.Pa
**Step 2:** Remove `Version` from each project's `PackageReference`:
```xml
<!-- BEFORE -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- AFTER -->
<PackageReference Include="Newtonsoft.Json" />
```
## Directory.Build Consolidation
Identify properties repeated across multiple `.csproj` files and move them to shared files.
**`Directory.Build.props`** (for properties — placed at repo or src root):
```xml
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Copyright>Copyright © Contoso 2024</Copyright>
</PropertyGroup>
</Project>
```
**`Directory.Build.targets`** (for targets/tasks — placed at repo or src root):
```xml
<Project>
<Target Name="PrintBuildInfo" AfterTargets="Build">
<Message Importance="High" Text="Built $(AssemblyName) → $(TargetPath)" />
</Target>
</Project>
```
**Keep in individual `.csproj` files** only what is project-specific:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<AssemblyName>MyApp</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" />
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
</ItemGroup>
</Project>
```
## Tools and Automation
| Tool | Usage |
|------|-------|
| `dotnet try-convert` | Automated legacy-to-SDK conversion. Install: `dotnet tool install -g try-convert` |
| .NET Upgrade Assistant | Full migration including API changes. Install: `dotnet tool install -g upgrade-assistant` |
| Visual Studio | Right-click `packages.config`*Migrate packages.config to PackageReference* |
| Manual migration | Often cleanest for simple projects — follow the checklist above |
**Recommended approach:**
1. Run `try-convert` for a first pass
2. Review and clean up the output manually
3. Build and fix any issues
4. Enable modern features (nullable, implicit usings)
5. Consolidate shared settings into `Directory.Build.props`
[truncated]
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env node
// Build entry point for the dotnet-msbuild component.
// Validates skills and compiles knowledge bundles.
// Run: node src/dotnet-msbuild/build.js
const fs = require("node:fs");
const path = require("node:path");
const SKILLS_DIR = path.resolve(__dirname, "skills");
const DOMAIN_GATE_PATTERN = /Only activate in MSBuild\/\.NET build context/;
// ── Step 1: Validate skills ─────────────────────────────────────────
console.log("=== Validating skills ===\n");
let errors = 0;
const skillDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory() && d.name !== "shared");
for (const dir of skillDirs) {
const skillFile = path.join(SKILLS_DIR, dir.name, "SKILL.md");
if (!fs.existsSync(skillFile)) continue;
const content = fs.readFileSync(skillFile, "utf-8");
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!match) {
console.error(`${dir.name}: Missing YAML frontmatter`);
errors++;
continue;
}
const frontmatter = match[1];
const descMatch = frontmatter.match(/description:\s*"([^"]*)"/);
if (!descMatch) {
console.error(`${dir.name}: Missing description in frontmatter`);
errors++;
continue;
}
const description = descMatch[1];
if (!DOMAIN_GATE_PATTERN.test(description)) {
console.error(`${dir.name}: Description missing domain gate. Must include 'Only activate in MSBuild/.NET build context.'`);
errors++;
}
}
if (errors > 0) {
console.error(`\n${errors} validation error(s) found.`);
process.exit(1);
} else {
console.log(`✅ All ${skillDirs.length} skills pass validation.\n`);
}
// ── Step 2: Compile knowledge bundles ────────────────────────────────
console.log("=== Compiling knowledge ===\n");
const KNOWLEDGE_TARGETS = {
"copilot-extension": {
outputDir: path.resolve(__dirname, "copilot-extension/src/knowledge"),
maxChars: 50000,
knowledgeMap: {
"build-errors": [
"binlog-failure-analysis",
"check-bin-obj-clash",
],
performance: [
"build-perf-baseline",
"build-perf-diagnostics",
"incremental-build",
"build-parallelism",
"eval-performance",
],
"style-guide": [
"msbuild-antipatterns",
"directory-build-organization",
"check-bin-obj-clash",
"including-generated-files",
],
modernization: [
"msbuild-modernization",
"directory-build-organization",
],
},
},
"agentic-workflows": {
outputDir: path.resolve(__dirname, "agentic-workflows/shared/compiled"),
maxChars: 40000,
knowledgeMap: {
"build-failure-knowledge": [
"binlog-failure-analysis",
"check-bin-obj-clash",
],
"pr-review-knowledge": [
"msbuild-antipatterns",
"msbuild-modernization",
"directory-build-organization",
"check-bin-obj-clash",
"incremental-build",
],
"perf-audit-knowledge": [
"build-perf-baseline",
"build-perf-diagnostics",
"incremental-build",
"build-parallelism",
"eval-performance",
],
},
},
};
function readSkill(skillName) {
const skillPath = path.join(SKILLS_DIR, skillName, "SKILL.md");
if (!fs.existsSync(skillPath)) {
console.warn(` ⚠ Skill not found: ${skillName} (${skillPath})`);
return null;
}
let content = fs.readFileSync(skillPath, "utf-8");
// Strip YAML frontmatter (tolerate both LF and CRLF)
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
if (frontmatterMatch) {
content = content.slice(frontmatterMatch[0].length);
}
return content.trim();
}
function compileKnowledgeFile(outputName, skillNames, outputDir, maxChars) {
const ext = ".lock.md";
console.log(` Compiling: ${outputName}${ext}`);
const sections = [];
let totalChars = 0;
const header = `<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->\n\n`;
totalChars += header.length;
for (const skillName of skillNames) {
const content = readSkill(skillName);
if (!content) continue;
if (totalChars + content.length > maxChars) {
console.warn(
` ⚠ Truncating ${skillName} — would exceed ${maxChars} char limit`
);
const remaining = maxChars - totalChars;
if (remaining > 500) {
sections.push(
`## ${skillName}\n\n${content.slice(0, remaining)}\n\n[truncated]`
);
totalChars += remaining;
}
break;
}
sections.push(content);
totalChars += content.length;
console.log(
`${skillName} (${content.length.toLocaleString()} chars)`
);
}
const output = header + sections.join("\n\n---\n\n");
const outputPath = path.join(outputDir, `${outputName}${ext}`);
fs.writeFileSync(outputPath, output, "utf-8");
console.log(
`${outputName}${ext} (${output.length.toLocaleString()} chars total)`
);
}
function compileTarget(targetName, config) {
console.log(`\n📦 Target: ${targetName}`);
console.log(` Output: ${config.outputDir}`);
fs.mkdirSync(config.outputDir, { recursive: true });
for (const [outputName, skillNames] of Object.entries(config.knowledgeMap)) {
compileKnowledgeFile(
outputName,
skillNames,
config.outputDir,
config.maxChars
);
}
}
console.log(`Skills source: ${SKILLS_DIR}`);
for (const [name, config] of Object.entries(KNOWLEDGE_TARGETS)) {
compileTarget(name, config);
}
console.log("\n✅ Build complete.");
+172
View File
@@ -0,0 +1,172 @@
# Build entry point for the dotnet-msbuild component.
# Validates skills and compiles knowledge bundles.
# Run: pwsh src/dotnet-msbuild/build.ps1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$SkillsDir = Join-Path $PSScriptRoot 'skills'
$DomainGatePattern = 'Only activate in MSBuild/\.NET build context'
# ── Step 1: Validate skills ─────────────────────────────────────────
Write-Host '=== Validating skills ===' -ForegroundColor Cyan
Write-Host ''
$errors = 0
$skillDirs = Get-ChildItem -Path $SkillsDir -Directory |
Where-Object { $_.Name -ne 'shared' }
foreach ($dir in $skillDirs) {
$skillFile = Join-Path $dir.FullName 'SKILL.md'
if (-not (Test-Path $skillFile)) { continue }
$content = Get-Content $skillFile -Raw
if ($content -notmatch '(?s)^---\s*\r?\n(.*?)\r?\n---') {
Write-Host "$($dir.Name): Missing YAML frontmatter" -ForegroundColor Red
$errors++
continue
}
$frontmatter = $Matches[1]
if ($frontmatter -notmatch 'description:\s*"([^"]*)"') {
Write-Host "$($dir.Name): Missing description in frontmatter" -ForegroundColor Red
$errors++
continue
}
$description = $Matches[1]
if ($description -notmatch $DomainGatePattern) {
Write-Host "$($dir.Name): Description missing domain gate. Must include 'Only activate in MSBuild/.NET build context.'" -ForegroundColor Red
$errors++
}
}
if ($errors -gt 0) {
Write-Host "`n$errors validation error(s) found." -ForegroundColor Red
exit 1
} else {
Write-Host "✅ All $($skillDirs.Count) skills pass validation.`n" -ForegroundColor Green
}
# ── Step 2: Compile knowledge bundles ────────────────────────────────
Write-Host '=== Compiling knowledge ===' -ForegroundColor Cyan
Write-Host ''
$KnowledgeGroups = [ordered]@{
'build-errors' = @(
'binlog-failure-analysis'
'binlog-generation'
'check-bin-obj-clash'
'including-generated-files'
)
'performance' = @(
'build-perf-baseline'
'build-perf-diagnostics'
'incremental-build'
'build-parallelism'
'eval-performance'
)
'style-and-modernization' = @(
'msbuild-antipatterns'
'msbuild-modernization'
'directory-build-organization'
)
}
$KnowledgeTargets = @{
'copilot-extension' = @{
OutputDir = Join-Path $PSScriptRoot 'copilot-extension' 'src' 'knowledge'
MaxChars = 50000
}
'agentic-workflows' = @{
OutputDir = Join-Path $PSScriptRoot 'agentic-workflows' 'shared' 'compiled'
MaxChars = 40000
}
}
function Read-Skill([string]$SkillName) {
$skillDir = Join-Path $SkillsDir $SkillName
$skillPath = Join-Path $skillDir 'SKILL.md'
if (-not (Test-Path $skillPath)) {
Write-Host " ⚠ Skill not found: $SkillName ($skillPath)" -ForegroundColor Yellow
return $null
}
$content = Get-Content $skillPath -Raw
# Strip YAML frontmatter (tolerate both LF and CRLF)
if ($content -match '(?s)^---\r?\n.*?\r?\n---\r?\n(.*)$') {
$content = $Matches[1]
}
# Inline linked references: replace [text](references/file.md) with file content
$content = [regex]::Replace($content, '\[([^\]]*)\]\((references/[^\)]+\.md)\)', {
param($m)
$refPath = Join-Path $skillDir $m.Groups[2].Value
if (Test-Path $refPath) {
$refContent = (Get-Content $refPath -Raw).Trim()
return $refContent
}
return $m.Value
})
return $content.Trim()
}
function Compile-KnowledgeFile([string]$OutputName, [string[]]$SkillNames, [string]$OutputDir, [int]$MaxChars) {
$ext = '.lock.md'
Write-Host " Compiling: $OutputName$ext"
$sections = [System.Collections.Generic.List[string]]::new()
$totalChars = 0
$header = "<!-- AUTO-GENERATED — DO NOT EDIT -->`n`n"
$totalChars += $header.Length
foreach ($skillName in $SkillNames) {
$content = Read-Skill $skillName
if ($null -eq $content) { continue }
if ($totalChars + $content.Length -gt $MaxChars) {
Write-Host " ⚠ Truncating $skillName — would exceed $MaxChars char limit" -ForegroundColor Yellow
$remaining = $MaxChars - $totalChars
if ($remaining -gt 500) {
$sections.Add("## $skillName`n`n$($content.Substring(0, $remaining))`n`n[truncated]")
$totalChars += $remaining
}
break
}
$sections.Add($content)
$totalChars += $content.Length
Write-Host "$skillName ($($content.Length.ToString('N0')) chars)"
}
$output = $header + ($sections -join "`n`n---`n`n")
$outputPath = Join-Path $OutputDir "$OutputName$ext"
[System.IO.File]::WriteAllText($outputPath, $output)
Write-Host "$OutputName$ext ($($output.Length.ToString('N0')) chars total)"
}
function Compile-Target([string]$TargetName, [hashtable]$Config) {
Write-Host "`n📦 Target: $TargetName"
Write-Host " Output: $($Config.OutputDir)"
New-Item -Path $Config.OutputDir -ItemType Directory -Force | Out-Null
foreach ($entry in $KnowledgeGroups.GetEnumerator()) {
Compile-KnowledgeFile -OutputName $entry.Key -SkillNames $entry.Value -OutputDir $Config.OutputDir -MaxChars $Config.MaxChars
}
}
Write-Host "Skills source: $SkillsDir"
foreach ($entry in $KnowledgeTargets.GetEnumerator()) {
Compile-Target -TargetName $entry.Key -Config $entry.Value
}
Write-Host "`n✅ Build complete." -ForegroundColor Green
+8 -10
View File
@@ -13,7 +13,7 @@ Users invoke it with `@msbuild` in any Copilot Chat:
## Architecture
This is a **serverless MVP** (Option A from the [design doc](../../../docs/copilot-extension-design.md)):
This is a **serverless MVP**:
- **Runtime**: Node.js (deployable to Azure Functions, AWS Lambda, Vercel, or any serverless platform)
- **Knowledge**: MSBuild skill content compiled into system prompts
@@ -44,11 +44,10 @@ copilot-extension/
│ ├── index.js # Entry point — handles Copilot webhook
│ ├── domain-check.js # MSBuild domain relevance detection
│ ├── intent-classifier.js # Routes to the right knowledge area
│ └── knowledge/ # Compiled skill content (generated by build.js)
│ ├── build-errors.lock.md # From binlog-failure-analysis + check-bin-obj-clash
│ ├── performance.lock.md # From build-perf-baseline + build-perf-diagnostics + incremental-build + build-parallelism + eval-performance
── style-guide.lock.md # From msbuild-antipatterns + directory-build-organization + check-bin-obj-clash + including-generated-files
│ └── modernization.lock.md # From msbuild-modernization + directory-build-organization
│ └── knowledge/ # Compiled skill content (generated by build.ps1)
│ ├── build-errors.lock.md # From binlog-failure-analysis + binlog-generation + check-bin-obj-clash + including-generated-files
│ ├── performance.lock.md # From build-perf-baseline + build-perf-diagnostics + incremental-build + build-parallelism + eval-performance
── style-and-modernization.lock.md # From msbuild-antipatterns + msbuild-modernization + directory-build-organization
└── app.yml # GitHub App manifest for registration
```
@@ -67,7 +66,7 @@ Extract and compile skill content into optimized knowledge files:
```bash
# From repo root
node src/dotnet-msbuild/build.js
pwsh src/dotnet-msbuild/build.ps1
# Or from copilot-extension/
npm run compile-knowledge
@@ -118,7 +117,7 @@ npm start
npm test
# Recompile knowledge after skill changes
node src/dotnet-msbuild/build.js
pwsh src/dotnet-msbuild/build.ps1
```
## How It Works
@@ -134,12 +133,11 @@ node src/dotnet-msbuild/build.js
## Extending
- **Add knowledge**: Update skills in `src/dotnet-msbuild/skills/`, then run `node src/dotnet-msbuild/build.js`
- **Add knowledge**: Update skills in `src/dotnet-msbuild/skills/`, then run `pwsh src/dotnet-msbuild/build.ps1`
- **Add intents**: Edit `src/intent-classifier.js` to recognize new categories
- **Add tools**: For v2, add MCP server integration for binlog analysis
## Related
- [Design Document](../../../docs/copilot-extension-design.md) — Full design rationale
- [Skills Repository](../) — Source knowledge base
- [GitHub Copilot Extensions Docs](https://docs.github.com/copilot/building-copilot-extensions)
@@ -5,7 +5,7 @@
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"compile-knowledge": "node ../build.js",
"compile-knowledge": "pwsh ../build.ps1",
"test": "node --test src/**/*.test.js"
},
"keywords": [
@@ -11,11 +11,11 @@ const INTENTS = {
description: "Build performance optimization",
},
STYLE_REVIEW: {
knowledgeKey: "style-guide",
knowledgeKey: "style-and-modernization",
description: "Project file quality and anti-patterns",
},
MODERNIZATION: {
knowledgeKey: "modernization",
knowledgeKey: "style-and-modernization",
description: "Legacy project modernization",
},
GENERAL: {
@@ -1,4 +1,4 @@
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
<!-- AUTO-GENERATED — DO NOT EDIT -->
# Analyzing MSBuild Failures with Binary Logs
@@ -285,6 +285,97 @@ When binlog analysis reveals these patterns, here's the fast path:
---
# Generate Binary Logs
**Pass the `/bl` switch when running any MSBuild-based command.** This is a non-negotiable requirement for all .NET builds.
## Commands That Require /bl
You MUST add the `/bl:{}` flag to:
- `dotnet build`
- `dotnet test`
- `dotnet pack`
- `dotnet publish`
- `dotnet restore`
- `msbuild` or `msbuild.exe`
- Any other command that invokes MSBuild
## Preferred: Use `{}` for Automatic Unique Names
> **Note:** The `{}` placeholder requires MSBuild 17.8+ / .NET 8 SDK or later.
The `{}` placeholder in the binlog filename is replaced by MSBuild with a unique identifier, guaranteeing no two builds ever overwrite each other — without needing to track or check existing files.
```bash
# Every invocation produces a distinct file automatically
dotnet build /bl:{}
dotnet test /bl:{}
dotnet build --configuration Release /bl:{}
```
**PowerShell requires escaping the braces:**
```powershell
# PowerShell: escape { } as {{ }}
dotnet build -bl:{{}}
dotnet test -bl:{{}}
```
## Why This Matters
1. **Unique names prevent overwrites** - You can always go back and analyze previous builds
2. **Failure analysis** - When a build fails, the binlog is already there for immediate analysis
3. **Comparison** - You can compare builds before and after changes
4. **No re-running builds** - You never need to re-run a failed build just to generate a binlog
## Examples
```bash
# ✅ CORRECT - {} generates a unique name automatically (bash/cmd)
dotnet build /bl:{}
dotnet test /bl:{}
# ✅ CORRECT - PowerShell escaping
dotnet build -bl:{{}}
dotnet test -bl:{{}}
# ❌ WRONG - Missing /bl flag entirely
dotnet build
dotnet test
# ❌ WRONG - No filename (overwrites the same msbuild.binlog every time)
dotnet build /bl
dotnet build /bl
```
## When a Specific Filename Is Required
If the binlog filename needs to be known upfront (e.g., for CI artifact upload), or if `{}` is not available in the installed MSBuild version, pick a name that won't collide with existing files:
1. Check for existing `*.binlog` files in the directory
2. Choose a name not already taken (e.g., by incrementing a counter from the highest existing number)
```bash
# Example: directory contains 3.binlog — use 4.binlog
dotnet build /bl:4.binlog
```
## Cleaning the Repository
When cleaning the repository with `git clean`, **always exclude binlog files** to preserve your build history:
```bash
# ✅ CORRECT - Exclude binlog files from cleaning
git clean -fdx -e "*.binlog"
# ❌ WRONG - This deletes binlog files (they're usually in .gitignore)
git clean -fdx
```
This is especially important when iterating on build fixes - you need the binlogs to analyze what changed between builds.
---
# Detecting OutputPath and IntermediateOutputPath Clashes
## Overview
@@ -620,4 +711,136 @@ When multiple evaluations share an output path, compare these global properties
## Testing Fixes
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
---
# Including Generated Files Into Your Build
## Overview
Files generated during the build are generally ignored by the build process. This leads to confusing results such as:
- Generated files not being included in the output directory
- Generated source files not being compiled
- Globs not capturing files created during the build
This happens because of how MSBuild's build phases work.
## Quick Takeaway
For code files generated during the build - we need to add those to `Compile` and `FileWrites` item groups within the target generating the file(s):
```xml
<ItemGroup>
<Compile Include="$(GeneratedFilePath)" />
<FileWrites Include="$(GeneratedFilePath)" />
</ItemGroup>
```
The target generating the file(s) should be hooked before CoreCompile and BeforeCompile targets - `BeforeTargets="CoreCompile;BeforeCompile"`
## Why Generated Files Are Ignored
For detailed explanation, see [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview).
### Evaluation Phase
MSBuild reads your project, imports everything, creates Properties, expands globs for Items **outside of Targets**, and sets up the build process.
### Execution Phase
MSBuild runs Targets & Tasks with the provided Properties & Items to perform the build.
**Key Takeaway:** Files generated during execution don't exist during evaluation, therefore they aren't found. This particularly affects files that are globbed by default, such as source files (`.cs`).
## Solution: Manually Add Generated Files
When files are generated during the build, manually add them into the build process. The approach depends on the type of file being generated.
### Use `$(IntermediateOutputPath)` for Generated File Location
Always use `$(IntermediateOutputPath)` as the base directory for generated files. **Do not** hardcode `obj\` or construct the intermediary path manually (e.g., `obj\$(Configuration)\$(TargetFramework)\`). The intermediate output path can be redirected to a different location in some build configurations (e.g., shared output directories, CI environments). Using `$(IntermediateOutputPath)` ensures your target works correctly regardless of the actual path.
### Always Add Generated Files to `FileWrites`
Every generated file should be added to the `FileWrites` item group. This ensures that MSBuild's `Clean` target properly removes your generated files. Without this, generated files will accumulate as stale artifacts across builds.
```xml
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
</ItemGroup>
```
### Basic Pattern (Non-Code Files)
For generated files that need to be copied to output (config files, data files, etc.), add them to `Content` or `None` items before `BeforeBuild`:
```xml
<Target Name="IncludeGeneratedFiles" BeforeTargets="BeforeBuild">
<!-- Your logic that generates files goes here -->
<ItemGroup>
<None Include="$(IntermediateOutputPath)my-generated-file.xyz" CopyToOutputDirectory="PreserveNewest"/>
<!-- Capture all files of a certain type with a glob -->
<None Include="$(IntermediateOutputPath)generated\*.xyz" CopyToOutputDirectory="PreserveNewest"/>
<!-- Register generated files for proper cleanup -->
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
<FileWrites Include="$(IntermediateOutputPath)generated\*.xyz" />
</ItemGroup>
</Target>
```
### For Generated Source Files (Code That Needs Compilation)
If you're generating `.cs` files that need to be compiled, use **`BeforeTargets="CoreCompile;BeforeCompile"`**. This is the correct timing for adding `Compile` items — it runs late enough that the file generation has occurred, but before the compiler runs. Using `BeforeBuild` is too early for some scenarios and may not work reliably with all SDK features.
```xml
<Target Name="IncludeGeneratedSourceFiles" BeforeTargets="CoreCompile;BeforeCompile">
<PropertyGroup>
<GeneratedCodeDir>$(IntermediateOutputPath)Generated\</GeneratedCodeDir>
<GeneratedFilePath>$(GeneratedCodeDir)MyGeneratedFile.cs</GeneratedFilePath>
</PropertyGroup>
<MakeDir Directories="$(GeneratedCodeDir)" />
<!-- Your logic that generates the .cs file goes here -->
<ItemGroup>
<Compile Include="$(GeneratedFilePath)" />
<FileWrites Include="$(GeneratedFilePath)" />
</ItemGroup>
</Target>
```
Note: Specifying both `CoreCompile` and `BeforeCompile` ensures the target runs before whichever target comes first, providing robust ordering regardless of customizations in the build.
## Target Timing
Choose the `BeforeTargets` value based on the type of file being generated:
- **`BeforeTargets="BeforeBuild"`** — For non-code files added to `None` or `Content`. Runs early enough for copy-to-output scenarios.
- **`BeforeTargets="CoreCompile;BeforeCompile"`** — For generated source files added to `Compile`. Ensures the file is included before the compiler runs.
- **`BeforeTargets="AssignTargetPaths"`** — The "final stop" before `None` and `Content` items (among others) are transformed into new items. Use as a fallback if `BeforeBuild` is too early.
## Globbing Behavior
Globs behave according to **when** the glob took place:
| Glob Location | Files Captured |
|---------------|----------------|
| Outside of a target | Only files visible during Evaluation phase (before build starts) |
| Inside of a target | Files visible when the target runs (can capture generated files if timed correctly) |
This is why the solution places the `<ItemGroup>` inside a `<Target>` - the glob runs during execution when the generated files exist.
## Relevant Links
- [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview)
- [Evaluation Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#evaluation-phase)
- [Execution Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#execution-phase)
- [Common Item Types](https://docs.microsoft.com/visualstudio/msbuild/common-msbuild-project-items)
- [How the SDK imports items by default](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.props)
- [Official docs: Handle generated files](https://learn.microsoft.com/visualstudio/msbuild/customize-your-build#handle-generated-files)
@@ -1,956 +0,0 @@
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
# MSBuild Modernization: Legacy to SDK-style Migration
## Identifying Legacy vs SDK-style Projects
**Legacy indicators:**
- `<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />`
- Explicit file lists (`<Compile Include="..." />` for every `.cs` file)
- `ToolsVersion` attribute on `<Project>` element
- `packages.config` file present
- `Properties\AssemblyInfo.cs` with assembly-level attributes
**SDK-style indicators:**
- `<Project Sdk="Microsoft.NET.Sdk">` attribute on root element
- Minimal content — a simple project may be 1015 lines
- No explicit file includes (implicit globbing)
- `<PackageReference>` items instead of `packages.config`
**Quick check:** if a `.csproj` is more than 50 lines for a simple class library or console app, it is likely legacy format.
```xml
<!-- Legacy: ~80+ lines for a simple library -->
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputType>Library</OutputType>
<RootNamespace>MyLibrary</RootNamespace>
<AssemblyName>MyLibrary</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<!-- ... 60+ more lines ... -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
```
```xml
<!-- SDK-style: ~8 lines for the same library -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
</Project>
```
## Migration Checklist: Legacy → SDK-style
### Step 1: Replace Project Root Element
**BEFORE:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<!-- ... project content ... -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
```
**AFTER:**
```xml
<Project Sdk="Microsoft.NET.Sdk">
<!-- ... project content ... -->
</Project>
```
Remove the XML declaration, `ToolsVersion`, `xmlns`, and both `<Import>` lines. The `Sdk` attribute replaces all of them.
### Step 2: Set TargetFramework
**BEFORE:**
```xml
<PropertyGroup>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
</PropertyGroup>
```
**AFTER:**
```xml
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
```
**TFM mapping table:**
| Legacy `TargetFrameworkVersion` | SDK-style `TargetFramework` |
|---------------------------------|-----------------------------|
| `v4.6.1` | `net461` |
| `v4.7.2` | `net472` |
| `v4.8` | `net48` |
| (migrating to .NET 6) | `net6.0` |
| (migrating to .NET 8) | `net8.0` |
### Step 3: Remove Explicit File Includes
**BEFORE:**
```xml
<ItemGroup>
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Models\User.cs" />
<Compile Include="Models\Order.cs" />
<Compile Include="Services\AuthService.cs" />
<Compile Include="Services\OrderService.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<!-- ... 50+ more lines ... -->
</ItemGroup>
<ItemGroup>
<Content Include="Views\Home\Index.cshtml" />
<Content Include="Views\Shared\_Layout.cshtml" />
<!-- ... more content files ... -->
</ItemGroup>
```
**AFTER:**
Delete all of these `<Compile>` and `<Content>` item groups entirely. SDK-style projects include them automatically via implicit globbing.
**Exception:** keep explicit entries only for files that need special metadata or reside outside the project directory:
```xml
<ItemGroup>
<Content Include="..\shared\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```
### Step 4: Remove AssemblyInfo.cs
**BEFORE** (`Properties\AssemblyInfo.cs`):
```csharp
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("MyLibrary")]
[assembly: AssemblyDescription("A useful library")]
[assembly: AssemblyCompany("Contoso")]
[assembly: AssemblyProduct("MyLibrary")]
[assembly: AssemblyCopyright("Copyright © Contoso 2024")]
[assembly: ComVisible(false)]
[assembly: Guid("...")]
[assembly: AssemblyVersion("1.2.0.0")]
[assembly: AssemblyFileVersion("1.2.0.0")]
```
**AFTER** (in `.csproj`):
```xml
<PropertyGroup>
<AssemblyTitle>MyLibrary</AssemblyTitle>
<Description>A useful library</Description>
<Company>Contoso</Company>
<Product>MyLibrary</Product>
<Copyright>Copyright © Contoso 2024</Copyright>
<Version>1.2.0</Version>
</PropertyGroup>
```
Delete `Properties\AssemblyInfo.cs` — the SDK auto-generates assembly attributes from these properties.
**Alternative:** if you prefer to keep `AssemblyInfo.cs`, disable auto-generation:
```xml
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
```
### Step 5: Migrate packages.config → PackageReference
**BEFORE** (`packages.config`):
```xml
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
<package id="Serilog" version="3.1.1" targetFramework="net472" />
<package id="Microsoft.Extensions.DependencyInjection" version="8.0.0" targetFramework="net472" />
</packages>
```
**AFTER** (in `.csproj`):
```xml
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
```
Delete `packages.config` after migration.
**Migration options:**
- **Visual Studio:** right-click `packages.config` → *Migrate packages.config to PackageReference*
- **CLI:** `dotnet migrate-packages-config` or manual conversion
- **Binding redirects:** SDK-style projects auto-generate binding redirects — remove the `<runtime>` section from `app.config` if present
### Step 6: Remove Unnecessary Boilerplate
Delete all of the following — the SDK provides sensible defaults:
```xml
<!-- DELETE: SDK imports (replaced by Sdk attribute) -->
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" ... />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- DELETE: default Configuration/Platform (SDK provides these) -->
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{...}</ProjectGuid>
<OutputType>Library</OutputType> <!-- keep only if not Library -->
<AppDesignerFolder>Properties</AppDesignerFolder>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<!-- DELETE: standard Debug/Release configurations (SDK defaults match) -->
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<!-- DELETE: framework assembly references (implicit in SDK) -->
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<!-- DELETE: packages.config reference -->
<None Include="packages.config" />
<!-- DELETE: designer service entries -->
<Service Include="{508349B6-6B84-11D3-8410-00C04F8EF8E0}" />
```
**Keep** only properties that differ from SDK defaults (e.g., `<OutputType>Exe</OutputType>`, `<RootNamespace>` if it differs from the assembly name, custom `<DefineConstants>`).
### Step 7: Enable Modern Features
After migration, consider enabling modern C# features:
```xml
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
</PropertyGroup>
```
- `<Nullable>enable</Nullable>` — enables nullable reference type analysis
- `<ImplicitUsings>enable</ImplicitUsings>` — auto-imports common namespaces (.NET 6+)
- `<LangVersion>latest</LangVersion>` — uses the latest C# language version (or specify e.g. `12.0`)
## Complete Before/After Example
**BEFORE** (legacy — 65 lines):
```xml
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{12345678-1234-1234-1234-123456789ABC}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MyLibrary</RootNamespace>
<AssemblyName>MyLibrary</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<Compile Include="Models\User.cs" />
<Compile Include="Models\Order.cs" />
<Compile Include="Services\UserService.cs" />
<Compile Include="Services\OrderService.cs" />
<Compile Include="Helpers\StringExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
```
**AFTER** (SDK-style — 11 lines):
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.1.1" />
</ItemGroup>
</Project>
```
## Common Migration Issues
**Embedded resources:** files not in a standard location may need explicit includes:
```xml
<ItemGroup>
<EmbeddedResource Include="..\shared\Schemas\*.xsd" LinkBase="Schemas" />
</ItemGroup>
```
**Content files with CopyToOutputDirectory:** these still need explicit entries:
```xml
<ItemGroup>
<Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="scripts\*.sql" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```
**Multi-targeting:** change the element name from singular to plural:
```xml
<!-- Single target -->
<TargetFramework>net8.0</TargetFramework>
<!-- Multiple targets -->
<TargetFrameworks>net472;net8.0</TargetFrameworks>
```
**WPF/WinForms projects:** use the appropriate SDK or properties:
```xml
<!-- Option A: WindowsDesktop SDK -->
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<!-- Option B: properties in standard SDK (preferred for .NET 5+) -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<UseWPF>true</UseWPF>
<!-- or -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
</Project>
```
**Test projects:** use the standard SDK with test framework packages:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
</ItemGroup>
</Project>
```
## Central Package Management Migration
Centralizes NuGet version management across a multi-project solution. See [https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) for details.
**Step 1:** Create `Directory.Packages.props` at the repository root with `<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>` and `<PackageVersion>` items for all packages.
**Step 2:** Remove `Version` from each project's `PackageReference`:
```xml
<!-- BEFORE -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- AFTER -->
<PackageReference Include="Newtonsoft.Json" />
```
## Directory.Build Consolidation
Identify properties repeated across multiple `.csproj` files and move them to shared files.
**`Directory.Build.props`** (for properties — placed at repo or src root):
```xml
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Copyright>Copyright © Contoso 2024</Copyright>
</PropertyGroup>
</Project>
```
**`Directory.Build.targets`** (for targets/tasks — placed at repo or src root):
```xml
<Project>
<Target Name="PrintBuildInfo" AfterTargets="Build">
<Message Importance="High" Text="Built $(AssemblyName) → $(TargetPath)" />
</Target>
</Project>
```
**Keep in individual `.csproj` files** only what is project-specific:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<AssemblyName>MyApp</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" />
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
</ItemGroup>
</Project>
```
## Tools and Automation
| Tool | Usage |
|------|-------|
| `dotnet try-convert` | Automated legacy-to-SDK conversion. Install: `dotnet tool install -g try-convert` |
| .NET Upgrade Assistant | Full migration including API changes. Install: `dotnet tool install -g upgrade-assistant` |
| Visual Studio | Right-click `packages.config`*Migrate packages.config to PackageReference* |
| Manual migration | Often cleanest for simple projects — follow the checklist above |
**Recommended approach:**
1. Run `try-convert` for a first pass
2. Review and clean up the output manually
3. Build and fix any issues
4. Enable modern features (nullable, implicit usings)
5. Consolidate shared settings into `Directory.Build.props`
---
# Organizing Build Infrastructure with Directory.Build Files
## Directory.Build.props vs Directory.Build.targets
Understanding which file to use is critical. They differ in **when** they are imported during evaluation:
**Evaluation order:**
```
Directory.Build.props → SDK .props → YourProject.csproj → SDK .targets → Directory.Build.targets
```
| Use `.props` for | Use `.targets` for |
|---|---|
| Setting property defaults | Custom build targets |
| Common item definitions | Late-bound property overrides |
| Properties projects can override | Post-build steps |
| Assembly/package metadata | Conditional logic on final values |
| Analyzer PackageReferences | Targets that depend on SDK-defined properties |
**Rule of thumb:** Properties and items go in `.props`. Custom targets and late-bound logic go in `.targets`.
Because `.props` is imported before the project file, the project can override any value set there. Because `.targets` is imported after everything, it gets the final say—but projects cannot override `.targets` values.
### ⚠️ Critical: TargetFramework Availability in .props vs .targets
**Property conditions on `$(TargetFramework)` in `.props` files silently fail for single-targeting projects** — the property is empty during `.props` evaluation. Move TFM-conditional properties to `.targets` instead. ItemGroup and Target conditions are not affected.
See the AP-21 section in the [msbuild-antipatterns skill](../msbuild-antipatterns/SKILL.md) for the full explanation.
## Directory.Build.props
### What to Put Here
**Output settings:**
```xml
<PropertyGroup>
<!-- Use with caution — see bin/obj clash skill for risks -->
<BaseOutputPath>$(MSBuildThisFileDirectory)artifacts\bin\</BaseOutputPath>
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)artifacts\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
</PropertyGroup>
```
**Language settings:**
```xml
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
```
**Assembly and package metadata:**
```xml
<PropertyGroup>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
<Copyright>Copyright © Contoso $(CurrentYear)</Copyright>
<Product>Contoso Platform</Product>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/contoso/platform</RepositoryUrl>
<PackageProjectUrl>https://github.com/contoso/platform</PackageProjectUrl>
</PropertyGroup>
```
**Build behavior and warnings:**
```xml
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
```
**Code analysis:**
```xml
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
```
**Common analyzer PackageReferences (apply to all projects):**
```xml
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
```
### What NOT to Put Here
- **Project-specific TFMs** — each project should declare its own `<TargetFramework>` or `<TargetFrameworks>`
- **Project-specific PackageReferences** — unless truly universal (e.g., analyzers for all projects)
- **Targets or complex build logic** — use `Directory.Build.targets` instead
- **Properties that depend on SDK-defined values** — those won't be available yet during `.props` evaluation
## Directory.Build.targets
### What to Put Here
**Custom build targets:**
```xml
<Target Name="ValidateProjectSettings" BeforeTargets="Build">
<Error Text="All libraries must target netstandard2.0 or higher"
Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
</Target>
```
**Conditional targets based on project type:**
```xml
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile"
Condition="'$(GenerateBuildInfo)' == 'true'">
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
Lines="[assembly: System.Reflection.AssemblyMetadata(&quot;BuildDate&quot;, &quot;$(Today)&quot;)]"
Overwrite="true" />
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
</ItemGroup>
</Target>
```
**Late-bound property overrides (values that depend on SDK properties):**
```xml
<PropertyGroup>
<!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
<DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
```
**Post-build validation:**
```xml
<Target Name="ValidatePackageOutput" AfterTargets="Pack"
Condition="'$(IsPackable)' == 'true'">
<Error Text="Package was not created at $(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg"
Condition="!Exists('$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg')" />
</Target>
```
## Directory.Packages.props (Central Package Management)
Central Package Management (CPM) provides a single source of truth for all NuGet package versions. See [https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) for details.
**Enable CPM in `Directory.Packages.props` at the repo root:**
```xml
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<!-- GlobalPackageReference applies to ALL projects — great for analyzers -->
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
</ItemGroup>
</Project>
```
## Directory.Build.rsp
Contains default MSBuild CLI arguments applied to all builds under the directory tree.
**Example `Directory.Build.rsp`:**
```
/maxcpucount
/nodeReuse:false
/consoleLoggerParameters:Summary;ForceNoAlign
/warnAsMessage:MSB3277
```
- Works with both `msbuild` and `dotnet` CLI in modern .NET versions
- Great for enforcing consistent CI and local build flags
- Each argument goes on its own line
## Multi-level Directory.Build Files
MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`) it finds walking up from the project directory. To chain multiple levels, you must explicitly import the parent.
**Add this at the TOP of inner `Directory.Build.props` files:**
```xml
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
<!-- Inner-level overrides go here -->
</Project>
```
**Example layout:**
```
repo/
Directory.Build.props ← repo-wide settings (lang version, company info, analyzers)
Directory.Build.targets ← repo-wide targets
Directory.Packages.props ← central package versions
src/
Directory.Build.props ← src-specific (imports repo-level, sets IsPackable=true)
MyLib/
MyLib.csproj
MyApp/
MyApp.csproj
test/
Directory.Build.props ← test-specific (imports repo-level, sets IsPackable=false)
MyLib.Tests/
MyLib.Tests.csproj
```
**Repo-level `Directory.Build.props`:**
```xml
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
```
**`src/Directory.Build.props`:**
```xml
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
<PropertyGroup>
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
```
**`test/Directory.Build.props`:**
```xml
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
</ItemGroup>
</Project>
```
## Common Patterns
### Pattern: Shared Analyzers via GlobalPackageReference
In `Directory.Packages.props`:
```xml
<ItemGroup>
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
</ItemGroup>
```
This ensures every project in the repo gets these analyzers without any per-project configuration.
### Pattern: Conditional Settings by Project Type
In `Directory.Build.props`:
```xml
<!-- Detect test projects by naming convention -->
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.UnitTests'))">
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
```
In `Directory.Build.targets`:
```xml
<!-- Detect project output type after SDK has set defaults -->
<PropertyGroup Condition="'$(OutputType)' == 'Exe'">
<SelfContained>false</SelfContained>
</PropertyGroup>
<PropertyGroup Condition="'$(OutputType)' == 'Library' AND '$(IsTestProject)' != 'true'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```
### Pattern: Before/After Repository Cleanup
**Before — duplicated settings in every .csproj:**
```xml
<!-- src/LibA/LibA.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
<!-- src/LibB/LibB.csproj — same boilerplate repeated -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
</Project>
```
**After — centralized with Directory.Build files:**
```xml
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
</Project>
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
</ItemGroup>
</Project>
<!-- src/LibA/LibA.csproj — clean and minimal -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>
<!-- src/LibB/LibB.csproj — clean and minimal -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>
</Project>
```
### Pattern: Artifact Output Layout (.NET 8+)
In `Directory.Build.props`:
```xml
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>
```
This produces a structured output layout:
```
artifacts/
bin/
MyLib/
debug/
release/
MyApp/
debug/
release/
obj/
MyLib/
MyApp/
publish/
MyApp/
```
The `ArtifactsPath` property (.NET 8+) automatically sets `BaseOutputPath`, `BaseIntermediateOutputPath`, and `PackageOutputPath` with project-name-separated directories, avoiding bin/obj clashes by default.
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| `Directory.Build.props` isn't picked up | File name casing wrong (exact match required on Linux/macOS) | Verify exact casing: `Directory.Build.props` (capital D, B) |
| Properties from `.props` are ignored by projects | Project sets the same property after the import | Move the property to `Directory.Build.targets` to set it after the project |
| Multi-level import doesn't work | Missing `GetPathOfFileAbove` import in inner file | Add the `<Import>` element at the top of the inner file (see Multi-level section) |
| Properties using SDK values are empty in `.props` | SDK properties aren't defined yet during `.props` evaluation | Move to `.targets` which is imported after the SDK |
| `Directory.Packages.props` not found | File not at repo root or not named exactly | Must be named `Directory.Packages.props` and at or above the project directory |
| Property condition on `$(TargetFramework)` doesn't match in `.props` | `TargetFramework` isn't set yet for single-targeting projects during `.props` evaluation | Move property to `.targets`, or use ItemGroup/Target conditions instead (which evaluate late) |
**Diagnosis:** Use the preprocessed project output to see all imports and final property values:
```bash
dotnet msbuild -pp:output.xml MyProject.csproj
```
This expands all imports inline so you can see exactly where each property is set and what the final evaluated value is.
@@ -1,4 +1,4 @@
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
<!-- AUTO-GENERATED — DO NOT EDIT -->
# Build Performance Baseline & Optimization
@@ -35,124 +35,40 @@ See the AP-21 section in the [msbuild-antipatterns skill](../msbuild-antipattern
## Directory.Build.props
### What to Put Here
**Output settings:**
Good candidates: language settings, assembly/package metadata, build warnings, code analysis, common analyzers.
```xml
<PropertyGroup>
<!-- Use with caution — see bin/obj clash skill for risks -->
<BaseOutputPath>$(MSBuildThisFileDirectory)artifacts\bin\</BaseOutputPath>
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)artifacts\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
</PropertyGroup>
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
</Project>
```
**Language settings:**
```xml
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
```
**Assembly and package metadata:**
```xml
<PropertyGroup>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
<Copyright>Copyright © Contoso $(CurrentYear)</Copyright>
<Product>Contoso Platform</Product>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/contoso/platform</RepositoryUrl>
<PackageProjectUrl>https://github.com/contoso/platform</PackageProjectUrl>
</PropertyGroup>
```
**Build behavior and warnings:**
```xml
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
```
**Code analysis:**
```xml
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
```
**Common analyzer PackageReferences (apply to all projects):**
```xml
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
```
### What NOT to Put Here
- **Project-specific TFMs** — each project should declare its own `<TargetFramework>` or `<TargetFrameworks>`
- **Project-specific PackageReferences** — unless truly universal (e.g., analyzers for all projects)
- **Targets or complex build logic** — use `Directory.Build.targets` instead
- **Properties that depend on SDK-defined values** — those won't be available yet during `.props` evaluation
**Do NOT put here:** project-specific TFMs, project-specific PackageReferences, targets/build logic, or properties depending on SDK-defined values (not available during `.props` evaluation).
## Directory.Build.targets
### What to Put Here
**Custom build targets:**
Good candidates: custom build targets, late-bound property overrides (values depending on SDK properties), post-build validation.
```xml
<Target Name="ValidateProjectSettings" BeforeTargets="Build">
<Error Text="All libraries must target netstandard2.0 or higher"
Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
</Target>
```
<Project>
<Target Name="ValidateProjectSettings" BeforeTargets="Build">
<Error Text="All libraries must target netstandard2.0 or higher"
Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
</Target>
**Conditional targets based on project type:**
```xml
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile"
Condition="'$(GenerateBuildInfo)' == 'true'">
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
Lines="[assembly: System.Reflection.AssemblyMetadata(&quot;BuildDate&quot;, &quot;$(Today)&quot;)]"
Overwrite="true" />
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
</ItemGroup>
</Target>
```
**Late-bound property overrides (values that depend on SDK properties):**
```xml
<PropertyGroup>
<!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
<DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
```
**Post-build validation:**
```xml
<Target Name="ValidatePackageOutput" AfterTargets="Pack"
Condition="'$(IsPackable)' == 'true'">
<Error Text="Package was not created at $(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg"
Condition="!Exists('$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg')" />
</Target>
<PropertyGroup>
<!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
<DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
</Project>
```
## Directory.Packages.props (Central Package Management)
@@ -201,9 +117,7 @@ Contains default MSBuild CLI arguments applied to all builds under the directory
## Multi-level Directory.Build Files
MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`) it finds walking up from the project directory. To chain multiple levels, you must explicitly import the parent.
**Add this at the TOP of inner `Directory.Build.props` files:**
MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`) it finds walking up from the project directory. To chain multiple levels, explicitly import the parent at the **top** of the inner file. See [multi-level-examples](references/multi-level-examples.md) for full file examples.
```xml
<Project>
@@ -218,227 +132,18 @@ MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`)
```
repo/
Directory.Build.props ← repo-wide settings (lang version, company info, analyzers)
Directory.Build.props ← repo-wide (lang version, company info, analyzers)
Directory.Build.targets ← repo-wide targets
Directory.Packages.props ← central package versions
src/
Directory.Build.props ← src-specific (imports repo-level, sets IsPackable=true)
MyLib/
MyLib.csproj
MyApp/
MyApp.csproj
test/
Directory.Build.props ← test-specific (imports repo-level, sets IsPackable=false)
MyLib.Tests/
MyLib.Tests.csproj
Directory.Build.props ← test-specific (imports repo-level, sets IsPackable=false, adds test packages)
```
**Repo-level `Directory.Build.props`:**
## Artifact Output Layout (.NET 8+)
```xml
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
```
**`src/Directory.Build.props`:**
```xml
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
<PropertyGroup>
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
```
**`test/Directory.Build.props`:**
```xml
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
</ItemGroup>
</Project>
```
## Common Patterns
### Pattern: Shared Analyzers via GlobalPackageReference
In `Directory.Packages.props`:
```xml
<ItemGroup>
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
</ItemGroup>
```
This ensures every project in the repo gets these analyzers without any per-project configuration.
### Pattern: Conditional Settings by Project Type
In `Directory.Build.props`:
```xml
<!-- Detect test projects by naming convention -->
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.UnitTests'))">
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
```
In `Directory.Build.targets`:
```xml
<!-- Detect project output type after SDK has set defaults -->
<PropertyGroup Condition="'$(OutputType)' == 'Exe'">
<SelfContained>false</SelfContained>
</PropertyGroup>
<PropertyGroup Condition="'$(OutputType)' == 'Library' AND '$(IsTestProject)' != 'true'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```
### Pattern: Before/After Repository Cleanup
**Before — duplicated settings in every .csproj:**
```xml
<!-- src/LibA/LibA.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
<!-- src/LibB/LibB.csproj — same boilerplate repeated -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
</Project>
```
**After — centralized with Directory.Build files:**
```xml
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
</Project>
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
</ItemGroup>
</Project>
<!-- src/LibA/LibA.csproj — clean and minimal -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>
<!-- src/LibB/LibB.csproj — clean and minimal -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>
</Project>
```
### Pattern: Artifact Output Layout (.NET 8+)
In `Directory.Build.props`:
```xml
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>
```
This produces a structured output layout:
```
artifacts/
bin/
MyLib/
debug/
release/
MyApp/
debug/
release/
obj/
MyLib/
MyApp/
publish/
MyApp/
```
The `ArtifactsPath` property (.NET 8+) automatically sets `BaseOutputPath`, `BaseIntermediateOutputPath`, and `PackageOutputPath` with project-name-separated directories, avoiding bin/obj clashes by default.
Set `<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>` in `Directory.Build.props` to automatically produce project-name-separated `bin/`, `obj/`, and `publish/` directories under a single `artifacts/` folder, avoiding bin/obj clashes by default. See [common-patterns](references/common-patterns.md) for the directory layout and additional patterns (conditional settings by project type, post-pack validation).
## Troubleshooting
@@ -0,0 +1,56 @@
# Common Directory.Build Patterns
## Conditional Settings by Project Type
Detect test projects by naming convention in `Directory.Build.props`:
```xml
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.UnitTests'))">
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
```
Use `Directory.Build.targets` for conditions on SDK-defined properties like `OutputType`:
```xml
<PropertyGroup Condition="'$(OutputType)' == 'Exe'">
<SelfContained>false</SelfContained>
</PropertyGroup>
<PropertyGroup Condition="'$(OutputType)' == 'Library' AND '$(IsTestProject)' != 'true'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```
## Post-Build Validation
Validate that `Pack` produced the expected output:
```xml
<Target Name="ValidatePackageOutput" AfterTargets="Pack"
Condition="'$(IsPackable)' == 'true'">
<Error Text="Package was not created at $(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg"
Condition="!Exists('$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg')" />
</Target>
```
## Artifact Output Layout (.NET 8+)
Setting `ArtifactsPath` in `Directory.Build.props` produces this structure:
```
artifacts/
bin/
MyLib/
debug/
release/
MyApp/
debug/
release/
obj/
MyLib/
MyApp/
publish/
MyApp/
```
@@ -0,0 +1,164 @@
# Multi-level Directory.Build Examples
Full file examples for a typical multi-level repo layout.
## Repo-level `Directory.Build.props`
```xml
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
```
## `src/Directory.Build.props`
```xml
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
<PropertyGroup>
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
```
## `test/Directory.Build.props`
```xml
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
</ItemGroup>
</Project>
```
## Before/After: Centralizing Duplicated Settings
**Before — duplicated settings in every .csproj:**
```xml
<!-- src/LibA/LibA.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
<!-- src/LibB/LibB.csproj — same boilerplate repeated -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
</Project>
```
**After — centralized with Directory.Build files:**
```xml
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Authors>Contoso Engineering</Authors>
</PropertyGroup>
</Project>
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
</ItemGroup>
</Project>
<!-- src/LibA/LibA.csproj — clean and minimal -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>
<!-- src/LibB/LibB.csproj — clean and minimal -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>
</Project>
```