For a comprehensive overview of MSBuild's evaluation and execution model, see
Build process overview
.
Initial properties
environment variables, global properties, reserved properties
Imports and property evaluation
process
, evaluate
top-to-bottom
Item definition evaluation
:
metadata defaults
Item evaluation
:
with
Include
,
Remove
,
Update
, glob expansion
UsingTask evaluation
register custom tasks
Key insight: evaluation happens BEFORE any targets run. Slow evaluation = slow build start even when nothing needs compiling.
Diagnosing Evaluation Performance
Using binlog
Replay the binlog:
dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log
Search for evaluation events:
grep -i 'Evaluation started|Evaluation finished' full.log
Multiple evaluations for the same project = overbuilding
Look for "Project evaluation started/finished" messages and their timestamps
Using /pp (preprocess)
dotnet msbuild -pp:full.xml MyProject.csproj
Shows the fully expanded project with ALL imports inlined
Use to understand: what's imported, import depth, total content volume
Large preprocessed output (>10K lines) = heavy evaluation
Using /clp:PerformanceSummary
Add to build command for timing breakdown
Shows evaluation time separately from target/task execution
Expensive Glob Patterns
Globs like
/*.cs
walk the entire directory tree
Default SDK globs are optimized, but custom globs may not be
Problem: globbing over
node_modules/
,
.git/
,
bin/
,
obj/
— millions of files
Fix: use
to exclude large directories
Fix: be specific with glob paths:
src//.cs
instead of
/.cs
Fix: use
false
only as last resort (lose SDK defaults)
Check: grep for Compile items in the diagnostic log → if Compile items include unexpected files, globs are too broad
Import Chain Analysis
Deep import chains (>20 levels) slow evaluation
Each import: file I/O + parse + evaluate
Common causes: NuGet packages adding .props/.targets, framework SDK imports, Directory.Build chains
Diagnosis:
/pp
output → search for
<!-- Importing
comments to see import tree
Fix: reduce transitive package imports where possible, consolidate imports
Multiple Evaluations
A project evaluated multiple times = wasted work
Common causes: referenced from multiple other projects with different global properties
Each unique set of global properties = separate evaluation
Diagnosis:
grep 'Evaluation started.*ProjectName' full.log
→ if count > 1, check for differing global properties
Fix: normalize global properties, use graph build (
/graph
)
TreatAsLocalProperty
Prevents property values from flowing to child projects via MSBuild task
Overuse: declaring many TreatAsLocalProperty entries adds evaluation overhead
Correct use: only when you genuinely need to override an inherited property
Property Function Cost
Property functions execute during evaluation
Most are cheap (string operations)
Expensive:
$([System.IO.File]::ReadAllText(...))
during evaluation — reads file on every evaluation
Expensive: network calls, heavy computation
Rule: property functions should be fast and side-effect-free
Optimization Checklist
Check preprocessed output size:
dotnet msbuild -pp:full.xml
Verify evaluation count: should be 1 per project per TFM
Exclude large directories from globs
Avoid file I/O in property functions during evaluation
Minimize import depth
Use graph build to reduce redundant evaluations
Check for unnecessary UsingTask declarations