<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Florin Vica</title>
    <description>The latest articles on DEV Community by Florin Vica (@florinvica).</description>
    <link>https://dev.to/florinvica</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3861033%2F81d03494-d8f8-4118-a656-f93eede16daa.jpeg</url>
      <title>DEV Community: Florin Vica</title>
      <link>https://dev.to/florinvica</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/florinvica"/>
    <language>en</language>
    <item>
      <title>9 Things That Silently Kill Your .NET Build Time (and How to Fix Each One)</title>
      <dc:creator>Florin Vica</dc:creator>
      <pubDate>Mon, 06 Apr 2026 16:46:24 +0000</pubDate>
      <link>https://dev.to/florinvica/9-things-that-silently-kill-your-net-build-time-and-how-to-fix-each-one-3kb4</link>
      <guid>https://dev.to/florinvica/9-things-that-silently-kill-your-net-build-time-and-how-to-fix-each-one-3kb4</guid>
      <description>&lt;p&gt;Every developer knows the pain: you hit Build, and then you &lt;em&gt;wait&lt;/em&gt;. A 2013 Electric Cloud survey of 443 engineers found developers spend &lt;strong&gt;3.5 hours per week&lt;/strong&gt; waiting on builds alone. Google's engineering productivity research team confirmed in a 2023 IEEE Software paper that even moderate reductions in build latency produce measurable gains in developer velocity and satisfaction — there is no "safe" threshold below which build time stops mattering.&lt;/p&gt;

&lt;p&gt;After fifteen years of working with large-scale MSBuild solutions — some with hundreds of projects — I've found the same nine culprits responsible for most of the pain. None of them announce themselves. They all silently compound. Here's how to find and fix each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Roslyn analyzers are eating 70% of your compile time
&lt;/h2&gt;

&lt;p&gt;The problem hides in plain sight. You add StyleCop.Analyzers for consistency, a security scanner for compliance, maybe AsyncFixer for good measure — and suddenly your build is four times slower. In &lt;a href="https://github.com/dotnet/roslyn/discussions/45933" rel="noopener noreferrer"&gt;dotnet/roslyn Discussion #45933&lt;/a&gt;, a developer reported a 160-project solution that built in &lt;strong&gt;1 minute 42 seconds&lt;/strong&gt; without analyzers but ballooned to &lt;strong&gt;8 minutes&lt;/strong&gt; with StyleCop and FxCop enabled. That's a 4.7× regression from code analysis alone.&lt;/p&gt;

&lt;p&gt;Anthony Simmon documented this extensively in his blog post &lt;a href="https://anthonysimmon.com/optimizing-csharp-code-analysis-for-quicker-dotnet-compilation/" rel="noopener noreferrer"&gt;"Optimizing C# code analysis for quicker .NET compilation"&lt;/a&gt;. In his real-world web solution, &lt;strong&gt;70% of build time was analyzers, 30% was actual compilation&lt;/strong&gt;. Disabling analysis dropped build time from ~2 minutes to under 50 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnose it&lt;/strong&gt; by setting &lt;code&gt;ReportAnalyzer&lt;/code&gt; to see per-analyzer timing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet build /p:ReportAnalyzer&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; /bl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the resulting &lt;code&gt;.binlog&lt;/code&gt; in &lt;a href="https://msbuildlog.com/" rel="noopener noreferrer"&gt;MSBuild Structured Log Viewer&lt;/a&gt; and expand the "Analyzer Summary" node. You'll see exactly which analyzers cost the most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; by running analyzers only in CI. Add this to your &lt;code&gt;Directory.Build.props&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RunAnalyzersDuringBuild&lt;/span&gt;
      &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"'$(CI)' != 'true' AND '$(TF_BUILD)' != 'true'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/RunAnalyzersDuringBuild&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties control analyzer behavior: &lt;code&gt;RunAnalyzersDuringBuild&lt;/code&gt; disables analyzers during build only (IDE live analysis still works), &lt;code&gt;RunAnalyzers&lt;/code&gt; kills both build and IDE analysis, and &lt;code&gt;EnforceCodeStyleInBuild&lt;/code&gt; enables IDE-style rules (IDExxxx) during CLI builds. For local development speed, toggling &lt;code&gt;RunAnalyzersDuringBuild&lt;/code&gt; is the surgical option. Expect &lt;strong&gt;50–70% build time reduction&lt;/strong&gt; on analyzer-heavy solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ResolveAssemblyReference scans thousands of directories silently
&lt;/h2&gt;

&lt;p&gt;MSBuild's &lt;code&gt;ResolveAssemblyReference&lt;/code&gt; (RAR) task maps every assembly reference in your project to an actual &lt;code&gt;.dll&lt;/code&gt; path on disk. The critical detail: &lt;strong&gt;RAR runs unconditionally on every build&lt;/strong&gt;, including incremental builds. As &lt;a href="https://github.com/dotnet/msbuild/issues/2015" rel="noopener noreferrer"&gt;dotnet/msbuild Issue #2015&lt;/a&gt; explains, the build system cannot know whether you've installed a new targeting pack since the last build, so it re-resolves every time.&lt;/p&gt;

&lt;p&gt;With .NET's micro-assembly model, modern projects pass hundreds of references to RAR. In &lt;a href="https://github.com/dotnet/msbuild/issues/6911" rel="noopener noreferrer"&gt;dotnet/msbuild Issue #6911&lt;/a&gt;, a developer with &lt;strong&gt;7,000 directories&lt;/strong&gt; in their packages folder reported RAR consuming &lt;strong&gt;30 minutes of total build time&lt;/strong&gt;. Even modest projects can suffer: one user in &lt;a href="https://github.com/dotnet/msbuild/discussions/9382" rel="noopener noreferrer"&gt;MSBuild Discussion #9382&lt;/a&gt; saw RAR jump from &lt;strong&gt;400ms to 3 seconds&lt;/strong&gt; per project after upgrading to Windows 11, traced to Smart App Control intercepting filesystem calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnose it&lt;/strong&gt; by generating a binlog (&lt;code&gt;dotnet build -bl&lt;/code&gt;) and searching for the &lt;code&gt;ResolveAssemblyReference&lt;/code&gt; task. Normal RAR time is &lt;strong&gt;100–500ms per project&lt;/strong&gt;. Anything above 2–3 seconds signals a problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; by trimming unnecessary search paths and ensuring your antivirus isn't amplifying the cost (see section 9). You can disable search paths you don't need in &lt;code&gt;Directory.Build.props&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;AssemblySearchPath_UseCandidateAssemblyFiles&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/AssemblySearchPath_UseCandidateAssemblyFiles&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;AssemblySearchPath_UseTargetFrameworkDirectory&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/AssemblySearchPath_UseTargetFrameworkDirectory&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also migrate away from &lt;code&gt;packages.config&lt;/code&gt; to &lt;code&gt;PackageReference&lt;/code&gt; format — it provides precise hint paths to RAR, drastically reducing directory scanning. For verbose diagnostics, set &lt;code&gt;MSBUILDLOGVERBOSERARSEARCHRESULTS=1&lt;/code&gt; before building.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Wildcard globs silently walk your entire disk
&lt;/h2&gt;

&lt;p&gt;SDK-style projects use default globs — &lt;code&gt;**/*.cs&lt;/code&gt; for Compile, &lt;code&gt;**/*&lt;/code&gt; for None — that recursively enumerate your entire project directory tree. This works fine until it doesn't. In &lt;a href="https://github.com/dotnet/msbuild/issues/2392" rel="noopener noreferrer"&gt;dotnet/msbuild Issue #2392&lt;/a&gt;, the MSBuild team documented that &lt;strong&gt;30–50% of build time&lt;/strong&gt; was spent searching the disk in large projects — particularly in design-time builds (IntelliSense, project-system updates), where evaluations taking 3 seconds should take 50–100ms. Full CLI builds are affected too, though typically less dramatically.&lt;/p&gt;

&lt;p&gt;The worst offender is &lt;a href="https://github.com/dotnet/msbuild/issues/8984" rel="noopener noreferrer"&gt;dotnet/msbuild Issue #8984&lt;/a&gt;, where glob expansion inside MSBuild targets was missing a critical exclude optimization — causing an internal project to spend &lt;strong&gt;over 10 minutes&lt;/strong&gt; in a single task because MSBuild recursed into &lt;code&gt;bin/&lt;/code&gt; and &lt;code&gt;obj/&lt;/code&gt; directories before subtracting them. And &lt;a href="https://github.com/dotnet/sdk/issues/49415" rel="noopener noreferrer"&gt;dotnet/sdk Issue #49415&lt;/a&gt; revealed that on a repo with 100K+ source files, &lt;strong&gt;2.25 minutes of a build was pure globbing&lt;/strong&gt;, with projects evaluated five separate times.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;node_modules&lt;/code&gt; folder is the classic trap. One &lt;code&gt;npm install&lt;/code&gt; drops thousands of nested directories into your project tree, and every &lt;code&gt;**/*.cs&lt;/code&gt; glob dutifully walks through all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnose it&lt;/strong&gt; by generating a binlog and profiling evaluation. The MSBuild evaluation profiler requires a file path argument and must be passed through &lt;code&gt;dotnet msbuild&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet msbuild &lt;span class="nt"&gt;-bl&lt;/span&gt; &lt;span class="nt"&gt;-profileevaluation&lt;/span&gt;:eval-perf.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the resulting binlog in the Structured Log Viewer and look at evaluation time, or review the generated Markdown report for per-project evaluation breakdowns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; by adding exclusions to &lt;code&gt;DefaultItemExcludes&lt;/code&gt; in &lt;code&gt;Directory.Build.props&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;DefaultItemExcludes&amp;gt;&lt;/span&gt;$(DefaultItemExcludes);node_modules/**;**/bower_components/**&lt;span class="nt"&gt;&amp;lt;/DefaultItemExcludes&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always prepend &lt;code&gt;$(DefaultItemExcludes)&lt;/code&gt; to preserve the default &lt;code&gt;bin/&lt;/code&gt; and &lt;code&gt;obj/&lt;/code&gt; exclusions. For projects where you control the file list explicitly, set &lt;code&gt;EnableDefaultCompileItems&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; and list files manually — this eliminates globbing entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. NuGet restore runs on every build when it shouldn't
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;dotnet build&lt;/code&gt; implicitly runs &lt;code&gt;dotnet restore&lt;/code&gt; first. Even when restore is a no-op — when &lt;code&gt;project.assets.json&lt;/code&gt; is already current — the evaluation overhead is real. NuGet must load and evaluate every project to determine whether restore is needed, and in large solutions this adds up fast.&lt;/p&gt;

&lt;p&gt;The performance gap can be staggering. &lt;a href="https://github.com/NuGet/Home/issues/11548" rel="noopener noreferrer"&gt;NuGet/Home Issue #11548&lt;/a&gt; documented a case where &lt;code&gt;dotnet restore&lt;/code&gt; took &lt;strong&gt;5 minutes 39 seconds on Windows&lt;/strong&gt; versus &lt;strong&gt;16 seconds on Ubuntu&lt;/strong&gt; for identical packages — because Windows was performing synchronous certificate revocation checks. Setting &lt;code&gt;NUGET_CERT_REVOCATION_MODE=offline&lt;/code&gt; dropped it to 1 minute 22 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; with three strategies. First, separate restore from build in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet restore
dotnet build &lt;span class="nt"&gt;--no-restore&lt;/span&gt; &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release
dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--no-build&lt;/span&gt; &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, enable static graph evaluation for a &lt;strong&gt;20–40% restore speedup&lt;/strong&gt; (&lt;a href="https://www.gresearch.com/news/improve-nuget-restores-with-static-graph-evaluation-2/" rel="noopener noreferrer"&gt;G-Research benchmarks&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;RestoreUseStaticGraphEvaluation&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/RestoreUseStaticGraphEvaluation&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, if you're on .NET 9+, the new dependency resolver is enabled by default — Microsoft reported an internal 2,500-project repo going from &lt;strong&gt;over 30 minutes to 2 minutes&lt;/strong&gt; for restore (the improvement came in two stages: optimizations to the legacy algorithm in .NET 8.0.300 halved restore time, then the rewritten resolver in .NET 9 brought it down to 2 minutes). For reproducibility, add &lt;code&gt;RestorePackagesWithLockFile&lt;/code&gt; and use &lt;code&gt;RestoreLockedMode&lt;/code&gt; in CI to fail on drift rather than silently re-resolving.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The CopyLocal avalanche copies hundreds of DLLs every build
&lt;/h2&gt;

&lt;p&gt;In a solution with &lt;em&gt;n&lt;/em&gt; projects, CopyLocal creates &lt;strong&gt;O(n²) file copy operations&lt;/strong&gt;. Each project copies its transitive dependencies — including every NuGet package DLL — to its own output directory. One developer in &lt;a href="https://github.com/dotnet/msbuild/issues/7014" rel="noopener noreferrer"&gt;dotnet/msbuild Issue #7014&lt;/a&gt; reported their binaries directory ballooning to &lt;strong&gt;~70 GB&lt;/strong&gt; from redundant copies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; with a layered approach. For library projects that don't need runtime dependencies in their output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;CopyLocalLockFileAssemblies&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/CopyLocalLockFileAssemblies&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For solutions where all projects share an output directory, eliminate copies entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;UseCommonOutputDirectory&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/UseCommonOutputDirectory&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;OutDir&amp;gt;&lt;/span&gt;$(SolutionDir)artifacts\bin\$(Configuration)\&lt;span class="nt"&gt;&amp;lt;/OutDir&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you can't avoid copies, replace them with near-instant NTFS hard links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;CreateHardLinksForCopyLocalIfPossible&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CreateHardLinksForCopyLocalIfPossible&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;CreateHardLinksForCopyFilesToOutputDirectoryIfPossible&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CreateHardLinksForCopyFilesToOutputDirectoryIfPossible&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;CreateHardLinksForPublishFilesIfPossible&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CreateHardLinksForPublishFilesIfPossible&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: hard links are &lt;a href="https://github.com/dotnet/msbuild/issues/2717" rel="noopener noreferrer"&gt;deliberately disabled inside Visual Studio&lt;/a&gt; because they share underlying file data — modifying one modifies all, which can corrupt the NuGet cache. Use them in CI and command-line builds. For solutions with 50+ projects, combining &lt;code&gt;UseCommonOutputDirectory&lt;/code&gt; with &lt;code&gt;CopyLocalLockFileAssemblies=false&lt;/code&gt; on libraries routinely saves &lt;strong&gt;minutes per build&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff warning:&lt;/strong&gt; &lt;code&gt;UseCommonOutputDirectory&lt;/code&gt; can break test isolation if two test projects emit conflicting assembly versions into the same folder. And hard links combined with aggressive CI caching can cause subtle cache poisoning. Profile first, apply selectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. You're building sequentially on a multi-core machine
&lt;/h2&gt;

&lt;p&gt;Here's a default that trips up many teams: &lt;strong&gt;standalone &lt;code&gt;msbuild.exe&lt;/code&gt; defaults to &lt;code&gt;/maxcpucount:1&lt;/code&gt;&lt;/strong&gt; — one core, fully sequential. If your CI pipeline calls &lt;code&gt;msbuild.exe&lt;/code&gt; directly, you're leaving all but one core idle. The &lt;code&gt;dotnet build&lt;/code&gt; command, however, already passes &lt;code&gt;-maxcpucount&lt;/code&gt; without a value to MSBuild — meaning it &lt;strong&gt;builds in parallel by default&lt;/strong&gt; using all available logical processors. Visual Studio's IDE also builds in parallel by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; — but only where it matters. If you invoke &lt;code&gt;msbuild.exe&lt;/code&gt; directly (CI scripts, legacy pipelines), add the &lt;code&gt;/m&lt;/code&gt; switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;msbuild MySolution.sln /m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;dotnet build&lt;/code&gt;, parallel is already the default — adding &lt;code&gt;-m&lt;/code&gt; is harmless but redundant. The &lt;a href="https://learn.microsoft.com/en-us/visualstudio/msbuild/building-multiple-projects-in-parallel-with-msbuild" rel="noopener noreferrer"&gt;MSBuild parallel build documentation&lt;/a&gt; explains the node architecture: each node is a separate worker process that builds one project at a time.&lt;/p&gt;

&lt;p&gt;Your actual speedup depends entirely on your dependency graph — and this is where many developers are disappointed. A linear chain A→B→C→D gets &lt;strong&gt;zero&lt;/strong&gt; benefit from parallelism — the critical path forces sequential execution. More commonly, solutions have a "hub" project (a shared core library, a data access layer) that 50+ projects depend on. That hub becomes the serialization bottleneck: nothing builds until it finishes, and &lt;code&gt;-m:16&lt;/code&gt; won't help. &lt;strong&gt;Wide, loosely-coupled solutions see the biggest gains&lt;/strong&gt;: community reports show 40–58% reductions on solutions with 70+ projects and shallow dependency trees.&lt;/p&gt;

&lt;p&gt;Before blindly adding &lt;code&gt;-m&lt;/code&gt;, visualize your actual parallelism by opening a binlog in the Structured Log Viewer's Timeline tab. Look for idle nodes — they indicate dependency bottlenecks. If most of your build timeline shows a single active node followed by a burst, the real fix isn't more cores — it's &lt;strong&gt;restructuring your project graph&lt;/strong&gt;. Split monolithic "Common" or "Core" projects into smaller, independent assemblies that can build concurrently. In solutions I've worked on, splitting a single 800-file hub project into 4 focused libraries improved parallel build time more than doubling the core count.&lt;/p&gt;

&lt;p&gt;For even better scheduling, try static graph builds with &lt;code&gt;/graph&lt;/code&gt;, which computes the full dependency DAG upfront and builds bottom-up for maximum parallelism.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Broken incremental builds force full recompilation every time
&lt;/h2&gt;

&lt;p&gt;This is arguably the &lt;strong&gt;most impactful and least discussed&lt;/strong&gt; build time killer. MSBuild's incremental build system relies on comparing &lt;code&gt;Inputs&lt;/code&gt; and &lt;code&gt;Outputs&lt;/code&gt; timestamps on every target. When this contract is broken — and it breaks silently — MSBuild rebuilds everything, every time. Developers notice that "clean build and normal build take the same time," shrug, and move on. Over weeks, the team just accepts 3-minute builds as normal when they should be 8-second no-ops.&lt;/p&gt;

&lt;p&gt;Common culprits that break incrementality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom targets without &lt;code&gt;Inputs&lt;/code&gt;/&lt;code&gt;Outputs&lt;/code&gt; attributes.&lt;/strong&gt; A &lt;code&gt;&amp;lt;Target Name="MyPreBuild" BeforeTargets="Build"&amp;gt;&lt;/code&gt; that runs a script or copies a file without declaring what it reads and writes forces MSBuild to re-run it unconditionally — and everything downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Targets that touch output files unnecessarily.&lt;/strong&gt; A code generator that rewrites a &lt;code&gt;.g.cs&lt;/code&gt; file with identical content still updates the timestamp, which cascades into a full recompile of everything that depends on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BeforeBuild&lt;/code&gt;/&lt;code&gt;AfterBuild&lt;/code&gt; targets that modify &lt;code&gt;bin/&lt;/code&gt; or &lt;code&gt;obj/&lt;/code&gt;.&lt;/strong&gt; Anything that writes into the output directory during build can confuse the up-to-date check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AssemblyInfo&lt;/code&gt; auto-generation with changing values.&lt;/strong&gt; If &lt;code&gt;GenerateAssemblyInfo&lt;/code&gt; includes a build timestamp or incrementing version on every build, you've just guaranteed that every build is a full build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File system timestamp issues.&lt;/strong&gt; Git operations (checkout, rebase) can reset timestamps on source files, triggering unnecessary rebuilds. Build machines that clone fresh repos hit this on every CI run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Diagnose it&lt;/strong&gt; by setting the &lt;code&gt;MSBUILDTARGETOUTPUTLOGGING=1&lt;/code&gt; environment variable, or more precisely, by using the &lt;a href="https://learn.microsoft.com/en-us/visualstudio/ide/how-to-configure-targets-and-tasks" rel="noopener noreferrer"&gt;MSBuild up-to-date check logs&lt;/a&gt;. In Visual Studio, set &lt;strong&gt;Tools → Options → Projects and Solutions → .NET Core → Up to date checks&lt;/strong&gt; logging to Verbose. Then build twice without changing anything. If the second build does real work, your incremental build is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; by auditing every custom target in your build. Ensure all targets declare &lt;code&gt;Inputs&lt;/code&gt; and &lt;code&gt;Outputs&lt;/code&gt;. For code generators, compare output content before writing — only write if the content actually changed. For assembly info, pin your version in CI and avoid timestamps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Deterministic&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/Deterministic&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;GenerateAssemblyInfo&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/GenerateAssemblyInfo&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Don't embed build time — it breaks incrementality --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A working incremental build should complete in &lt;strong&gt;under 2 seconds&lt;/strong&gt; for a no-change rebuild of a 50-project solution. If yours takes more than 10 seconds with no changes, you have a broken target somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The compiler server isn't running (or keeps dying)
&lt;/h2&gt;

&lt;p&gt;The Roslyn compiler ships with &lt;code&gt;VBCSCompiler.exe&lt;/code&gt;, a long-running server process that keeps the compiler loaded in memory between builds. Without it, every project invocation pays the full &lt;strong&gt;JIT and assembly load cost&lt;/strong&gt; of starting the C# compiler from scratch — estimated at several hundred milliseconds per project based on community profiling. On a 100-project solution, that overhead compounds quickly into a significant penalty.&lt;/p&gt;

&lt;p&gt;SDK-style projects enable the compiler server by default via &lt;code&gt;/shared&lt;/code&gt;. But several common scenarios silently disable or destabilize it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mixed solutions with legacy &lt;code&gt;.csproj&lt;/code&gt; (non-SDK-style) and &lt;code&gt;.vcxproj&lt;/code&gt; projects.&lt;/strong&gt; The MSBuild nodes spawned for native C++ builds don't share the compiler server context, and in some configurations the server doesn't start at all for the managed projects in the same build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI environments that kill long-running processes.&lt;/strong&gt; If your CI agent terminates &lt;code&gt;VBCSCompiler.exe&lt;/code&gt; between build steps (some Docker-based agents do this), every step pays the cold-start penalty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UseSharedCompilation=false&lt;/code&gt; set somewhere in your props chain.&lt;/strong&gt; Sometimes added intentionally for deterministic builds, sometimes inherited from a NuGet package's &lt;code&gt;.props&lt;/code&gt; file without anyone noticing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server timeout too aggressive.&lt;/strong&gt; The default &lt;code&gt;VBCSCompiler&lt;/code&gt; idle timeout is 10 minutes (&lt;code&gt;/keepalive:600&lt;/code&gt;). In CI pipelines with gaps between builds, the server may shut down between steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Diagnose it&lt;/strong&gt; by checking if the server is alive during builds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# During or right after a build:&lt;/span&gt;
tasklist /fi &lt;span class="s2"&gt;"imagename eq VBCSCompiler.exe"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the process isn't there, or if you see it spawning and dying repeatedly in Process Monitor, the server isn't providing its intended benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; by ensuring &lt;code&gt;UseSharedCompilation&lt;/code&gt; isn't disabled anywhere in your import chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet build /bl
&lt;span class="c"&gt;# In binlog viewer, search for "UseSharedCompilation" in properties&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find it set to &lt;code&gt;false&lt;/code&gt;, trace which &lt;code&gt;.props&lt;/code&gt; or &lt;code&gt;.targets&lt;/code&gt; file sets it. For CI, explicitly keep the server alive across steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;UseSharedCompilation&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/UseSharedCompilation&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in CI scripts, avoid killing the process between build steps. If you must run in an isolated environment, at least keep the server alive within a single build invocation — the savings compound across projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Windows Defender intercepts every file your build writes
&lt;/h2&gt;

&lt;p&gt;Real-time antivirus protection synchronously scans every file open, create, and write operation during your build. Steve Smith (Ardalis) &lt;a href="https://ardalis.com/speed-up-visual-studio-build-times/" rel="noopener noreferrer"&gt;documented&lt;/a&gt; that Defender's Antimalware Service Executable consumed nearly as much CPU as Visual Studio itself during builds. In extreme cases — particularly in I/O-heavy solutions with thousands of assembly references — Defender overhead can push build times into multiples of their unscanned baseline. Community reports consistently show &lt;strong&gt;30–60% improvements&lt;/strong&gt; after adding appropriate Defender exclusions for build paths and processes, though individual results vary significantly based on project size and I/O patterns.&lt;/p&gt;

&lt;p&gt;The best modern fix is &lt;strong&gt;Developer Drive&lt;/strong&gt;, introduced in Windows 11 23H2. Dev Drive uses the &lt;strong&gt;ReFS filesystem&lt;/strong&gt; with optimizations for developer I/O patterns and enables Microsoft Defender's &lt;strong&gt;performance mode&lt;/strong&gt; — an asynchronous scanning mode that defers security checks until after file operations complete instead of blocking them. Microsoft's &lt;a href="https://devblogs.microsoft.com/engineering-at-microsoft/dev-drive-and-copy-on-write-for-developer-performance/" rel="noopener noreferrer"&gt;engineering blog&lt;/a&gt; reported &lt;strong&gt;14% faster builds&lt;/strong&gt; from Dev Drive alone on a 500+ project C# codebase, and &lt;strong&gt;28% total&lt;/strong&gt; with the &lt;code&gt;Microsoft.Build.CopyOnWrite&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveats on Dev Drive adoption:&lt;/strong&gt; Moving an established repo and its toolchain to a ReFS Dev Drive isn't trivial. You need to relocate your source tree, NuGet package cache (&lt;code&gt;NUGET_PACKAGES&lt;/code&gt;), and potentially the .NET SDK itself to the Dev Drive to see full benefits. ReFS doesn't support NTFS compression (relevant if you're space-constrained), isn't available on Windows 10, and some third-party tools have quirks with ReFS paths. Profile on a test workstation before committing the team to a migration.&lt;/p&gt;

&lt;p&gt;If you can't use Dev Drive, add exclusions for build-critical paths and processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Process exclusions (highest impact)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionProcess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionProcess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MSBuild.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionProcess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devenv.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionProcess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VBCSCompiler.exe"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Path exclusions&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\.nuget\packages"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files\dotnet"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\Projects"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# your source root&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify your Dev Drive trust status with &lt;code&gt;fsutil devdrv query D:&lt;/code&gt;. Microsoft explicitly states that performance mode provides &lt;strong&gt;better security&lt;/strong&gt; than folder exclusions, since files are still scanned — just asynchronously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference: all fixes in one place
&lt;/h2&gt;

&lt;p&gt;Drop this into your &lt;code&gt;Directory.Build.props&lt;/code&gt; to apply the non-destructive fixes solution-wide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- 1. Analyzers: CI only --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RunAnalyzersDuringBuild&lt;/span&gt;
      &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"'$(CI)' != 'true'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/RunAnalyzersDuringBuild&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- 3. Globs: exclude heavy directories --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DefaultItemExcludes&amp;gt;&lt;/span&gt;$(DefaultItemExcludes);node_modules/**&lt;span class="nt"&gt;&amp;lt;/DefaultItemExcludes&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- 4. NuGet: static graph restore --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RestoreUseStaticGraphEvaluation&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/RestoreUseStaticGraphEvaluation&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- 5. CopyLocal: hard links on CLI builds --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CreateHardLinksForCopyLocalIfPossible&lt;/span&gt;
      &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"'$(BuildingInsideVisualStudio)' != 'true'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CreateHardLinksForCopyLocalIfPossible&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CreateHardLinksForCopyFilesToOutputDirectoryIfPossible&lt;/span&gt;
      &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"'$(BuildingInsideVisualStudio)' != 'true'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CreateHardLinksForCopyFilesToOutputDirectoryIfPossible&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- 7. Incremental builds: deterministic output --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Deterministic&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/Deterministic&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- 8. Compiler server: ensure it's enabled --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;UseSharedCompilation&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/UseSharedCompilation&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in your CI pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet restore
dotnet build &lt;span class="nt"&gt;--no-restore&lt;/span&gt; &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release
dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--no-build&lt;/span&gt; &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release
&lt;span class="c"&gt;# If using msbuild.exe directly instead of dotnet build, add /m for parallel:&lt;/span&gt;
&lt;span class="c"&gt;# msbuild MySolution.sln /m /p:Configuration=Release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These nine fixes target different layers of the build pipeline — compiler integration, assembly resolution, filesystem evaluation, package management, output copying, CPU utilization, incremental build hygiene, compiler server management, and OS-level I/O — but they share a pattern: &lt;strong&gt;they're all defaults or oversights that make sense for small projects and silently degrade at scale&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The actual improvement you'll see depends heavily on your specific solution's profile. On 100+ project solutions where several of these issues compound, I've seen total build times drop significantly — but the gains vary. A solution dominated by analyzer overhead will see dramatic improvement from fix #1 alone. A solution with a broken incremental build might see the biggest win from auditing custom targets. &lt;strong&gt;There is no universal "apply these five XML properties and get 50% faster builds."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The diagnostic approach matters as much as the fixes: generate a binlog with &lt;code&gt;dotnet build -bl&lt;/code&gt;, open it in the &lt;a href="https://msbuildlog.com/" rel="noopener noreferrer"&gt;Structured Log Viewer&lt;/a&gt;, and let the data tell you where &lt;em&gt;your specific build&lt;/em&gt; bleeds time. Measure before and after every change. Every solution is different, but these nine culprits account for the vast majority of preventable build waste.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>msbuild</category>
      <category>performance</category>
      <category>csharp</category>
    </item>
    <item>
      <title>I Built an MCP Server That Understands Your MSBuild Project Graph — Before You Build</title>
      <dc:creator>Florin Vica</dc:creator>
      <pubDate>Sat, 04 Apr 2026 19:25:32 +0000</pubDate>
      <link>https://dev.to/florinvica/i-built-an-mcp-server-that-understands-your-msbuild-project-graph-before-you-build-1pc8</link>
      <guid>https://dev.to/florinvica/i-built-an-mcp-server-that-understands-your-msbuild-project-graph-before-you-build-1pc8</guid>
      <description>&lt;p&gt;Ask your AI coding assistant about your .NET solution structure and watch it hallucinate. It'll guess at project references, miss TFM mismatches, and confidently tell you things that aren't true — because it has no way to actually &lt;em&gt;evaluate&lt;/em&gt; your MSBuild project files.&lt;/p&gt;

&lt;p&gt;Existing tools like BinlogInsights require you to build first, then analyze the binary log. That's useful, but it means you need a successful build before you can ask questions. What if your solution is broken? What if you just want to understand the dependency graph before a migration?&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/FlorinVica/msbuild-graph-mcp" rel="noopener noreferrer"&gt;MSBuild Graph MCP Server&lt;/a&gt; to fill this gap. It evaluates MSBuild project files directly — no build required — and exposes the results through 10 MCP tools that any AI assistant can call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Install it as a .NET global tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; MsBuildGraphMcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask your assistant natural questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"Show me the dependency graph for this solution"&lt;/em&gt; → full DAG with topological sort&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Are there any TFM mismatches?"&lt;/em&gt; → finds net6.0 projects referencing net8.0 libraries&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"What breaks if I remove CoreLib?"&lt;/em&gt; → BFS traversal of all direct + transitive dependents&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Compare Debug vs Release"&lt;/em&gt; → property and package reference diffs&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Where does LangVersion come from?"&lt;/em&gt; → traces to Directory.Build.props, line 3&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  10 Tools, Grouped by Purpose
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Understand Structure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;analyze_solution&lt;/code&gt; — parse .sln, .slnx, .slnf with full project metadata&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_project_graph&lt;/code&gt; — dependency DAG, topological sort, graph metrics&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;find_shared_imports&lt;/code&gt; — Directory.Build.props/.targets discovery&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_projects&lt;/code&gt; — fast listing, no MSBuild evaluation overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Find Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;detect_build_issues&lt;/code&gt; — TFM mismatches, orphans, circular deps, platform conflicts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_package_versions&lt;/code&gt; — NuGet version consistency, CPM detection, VersionOverride&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Analyze Impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;analyze_impact&lt;/code&gt; — "what breaks if I touch project X?"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_build_order&lt;/code&gt; — topological sort with critical path length&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Compare &amp;amp; Inspect:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;compare_configurations&lt;/code&gt; — diff any two build configurations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyze_project_properties&lt;/code&gt; — property values with source file + line tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus &lt;strong&gt;2 guided prompts&lt;/strong&gt;: &lt;code&gt;project-health-check&lt;/code&gt; (scores your solution 1-10) and &lt;code&gt;migration-readiness&lt;/code&gt; (assesses .NET version upgrade feasibility).&lt;/p&gt;

&lt;h2&gt;
  
  
  We Predict. They Report.
&lt;/h2&gt;

&lt;p&gt;Every other MSBuild MCP server does &lt;strong&gt;post-build&lt;/strong&gt; analysis — they parse binary logs after compilation. That's retrospective. We do &lt;strong&gt;pre-build&lt;/strong&gt; analysis: evaluating project files directly through MSBuild's ProjectGraph API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;MSBuild Graph MCP&lt;/th&gt;
&lt;th&gt;Binlog tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requires build&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on broken solutions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency analysis&lt;/td&gt;
&lt;td&gt;Full DAG&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TFM compatibility checking&lt;/td&gt;
&lt;td&gt;Yes (NuGet.Frameworks)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impact analysis&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration diff&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This matters when you're planning a migration, onboarding to a large codebase, or debugging build issues in a solution that won't compile yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: 15 Measures, Zero Side Effects
&lt;/h2&gt;

&lt;p&gt;All tools are &lt;strong&gt;read-only&lt;/strong&gt;. No builds triggered, no files modified, no network requests, no arbitrary commands.&lt;/p&gt;

&lt;p&gt;Highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Startup guard&lt;/strong&gt; blocks &lt;code&gt;MSBUILDENABLEALLPROPERTYFUNCTIONS&lt;/code&gt; — mitigates CVE-2025-21172 (property function RCE)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-evaluation XML scanner&lt;/strong&gt; detects &lt;code&gt;System.IO.File&lt;/code&gt;, &lt;code&gt;System.Net&lt;/code&gt;, &lt;code&gt;System.Diagnostics&lt;/code&gt; in project files &lt;em&gt;before&lt;/em&gt; MSBuild evaluates them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IsBuildEnabled = false&lt;/strong&gt; on all ProjectCollection instances — prevents target execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNC path rejection&lt;/strong&gt;, extension whitelist, symlink detection, input length caps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error sanitization&lt;/strong&gt; strips user paths and stack traces from responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowed directories&lt;/strong&gt; via &lt;code&gt;MSBUILD_MCP_ALLOWED_PATHS&lt;/code&gt; environment variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MSBuild property functions execute during evaluation by design — this is the same trust model as opening a project in Visual Studio. Only analyze projects you trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  333 Tests, 8 Bugs Caught
&lt;/h2&gt;

&lt;p&gt;The test suite runs against &lt;strong&gt;real MSBuild APIs&lt;/strong&gt; — no mocks. A &lt;code&gt;TempSolutionBuilder&lt;/code&gt; fixture creates actual .sln/.slnx/.csproj files in temp directories for every test scenario.&lt;/p&gt;

&lt;p&gt;This approach caught 8 production bugs during development, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CircularDependencyException&lt;/code&gt; not being caught (MSBuild throws this separately from &lt;code&gt;MSB4251&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ToDictionary&lt;/code&gt; crash on duplicate PackageReferences (needed &lt;code&gt;GroupBy&lt;/code&gt; first)&lt;/li&gt;
&lt;li&gt;Resource leaks on exception paths (added &lt;code&gt;try/finally&lt;/code&gt; cleanup)&lt;/li&gt;
&lt;li&gt;Unbounded parallelism on 64-core machines (capped at 8)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All 333 tests pass in ~12 seconds on CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; MsBuildGraphMcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt; — add to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msbuild-graph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"msbuild-graph-mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;VS Code&lt;/strong&gt; — add to &lt;code&gt;.vscode/mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msbuild-graph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"msbuild-graph-mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add msbuild-graph &lt;span class="nt"&gt;--&lt;/span&gt; msbuild-graph-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also works with &lt;strong&gt;Cursor&lt;/strong&gt;, &lt;strong&gt;Windsurf&lt;/strong&gt;, and &lt;strong&gt;Visual Studio 2026 Preview&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Requires .NET SDK 8.0+ and Windows (MSBuildLocator discovers VS/.NET SDK installations).&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install with &lt;code&gt;dotnet tool install -g MsBuildGraphMcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Point your AI assistant at a .NET solution&lt;/li&gt;
&lt;li&gt;Ask: &lt;em&gt;"Run a project health check on this solution"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;project-health-check&lt;/code&gt; prompt runs all 10 tools and produces a scored report with actionable recommendations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/FlorinVica/msbuild-graph-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/MsBuildGraphMcp" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. Contributions welcome.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>mcp</category>
      <category>ai</category>
      <category>msbuild</category>
    </item>
  </channel>
</rss>
