Wiring Up Static Code Analysis via Directory.Build.props
A concrete Directory.Build.props example that incorporates Microsoft's .NET analyzers and StyleCop Analyzers, and applies stricter analysis to production projects than to tests and tooling.
Deciding on a static analysis rule set is one thing; actually wiring it into a repository so every project picks it up automatically is another. Directory.Build.props is the mechanism that makes it automatic — MSBuild picks it up implicitly for every project under it, without each .csproj needing to reference anything explicitly. Here’s a working example, covering both the shared baseline and the split between production and non-production strictness described in the companion post on the agreed rule set.
The full file
<!-- This content covers only the essential parts to incorporate and configure SCA-->
<!-- Re-use it and massage the content for your concrete purposes -->
<Project>
<!-- Incorporate 1st party analyzers via Nuget package -->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
<Version>8.0.0</Version>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<!-- Incorporate StyleCop analyzers via Nuget package -->
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory).stylecop.json" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup Label="Default configuration for projects, all in for analyzers and rules!">
<!-- Enable the built-in .NET analyzers from SDK -->
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<!-- Enable or disable analyzer execution globally -->
<RunAnalyzers>true</RunAnalyzers>
<!-- Control whether analyzers run during builds -->
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<!-- Enforce code style rules during the build -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Set the analysis level -->
<AnalysisLevel>latest</AnalysisLevel>
<!-- Define the scope of rules -->
<AnalysisMode>All</AnalysisMode> <!-- Enables all available diagnostic rules, including those disabled by default -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<Choose>
<When Condition="!$(MSBuildProjectName.EndsWith('Tests')) And !$(MSBuildProjectName.EndsWith('TestUtilities'))">
<PropertyGroup Label="Production projects with strict code analysis">
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup Label="Non-production projects with more relaxed code analysis">
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<AnalysisLevel>latest-minimum</AnalysisLevel>
<NoWarn>$(NoWarn);CS1591</NoWarn> <!-- Missing XML comment for publicly visible type or member -->
</PropertyGroup>
</Otherwise>
</Choose>
</Project>
Walking through the pieces
Two analyzer packages, two responsibilities. Microsoft.CodeAnalysis.NetAnalyzers covers correctness and framework-level best practices; StyleCop.Analyzers covers style and layout, configured further via the referenced .stylecop.json file. Splitting them keeps each one doing the job it’s actually good at.
A permissive-looking baseline. The default PropertyGroup turns everything on — all analyzers, all analysis levels, style enforcement during the build, XML documentation generation. This is the shared starting point every project inherits before the production/non-production split narrows it back down.
The actual split happens in Choose/When/Otherwise. Any project whose name doesn’t end in Tests or TestUtilities is treated as production code and held to latest-recommended analysis. Everything else — test projects and test utility projects — gets the more relaxed latest-minimum level, plus a suppression for CS1591 (missing XML doc comments), which is exactly the kind of rule that adds real value on a public API and pure noise on a test fixture.
The naming-convention check (EndsWith('Tests') / EndsWith('TestUtilities')) is doing a lot of work here — it’s what lets a single shared file apply different strictness without anyone having to opt individual projects in or out by hand. Get your test project naming consistent, and this split just works.
A couple of things worth knowing before you copy this
A few lines in the original configuration were left in deliberately as open questions rather than settled answers, and it’s worth calling them out rather than presenting them as more authoritative than they are:
- The commented-out
IncludeAssetsline on the Microsoft analyzers package reference was seen used elsewhere, but its actual effect wasn’t fully pinned down — worth testing in your own setup rather than assuming it’s required. - Whether
RunAnalyzersDuringBuildset totruevs.falsemade an observable difference wasn’t conclusively determined at the time this was written — it’s set totruehere, but treat that as a starting point rather than a verified requirement. EnforceCodeStyleInBuildonly affects code-style (IDExxxx) rules specifically — worth knowing if you’re trying to figure out why a style rule isn’t showing up as a build warning even with the property set.
The takeaway
The interesting part of this file isn’t any single property — it’s the Choose/When block that lets one Directory.Build.props apply meaningfully different strictness to production and test code automatically, based on naming convention alone. That’s what turns “we agreed on a rule set” into something that’s actually enforced consistently without per-project configuration drift.