When setting up a security-gated .NET build pipeline, two MSBuild properties cause confusion: BTreatWarningsAsErrors and the related concept of TargetRules for scoping which Roslyn analyzer warnings become build errors. Getting these right means your CI pipeline blocks on real security violations without generating noise from auto-generated or design-time code.
This guide covers:
- The difference between
TreatWarningsAsErrors,BTreatWarningsAsErrors, andWarningsAsErrors - How to scope
TargetRulesto specific security CA rules - The correct MSBuild configuration for enforcing Roslyn security analysis in CI/CD
- Why
dotnet efmigrations fight withTreatWarningsAsErrors=true— and the safe fix - What Roslyn enforcement misses (and when you need full taint-analysis SAST)
TreatWarningsAsErrors vs BTreatWarningsAsErrors
MSBuild has several overlapping properties for promoting warnings to errors. Their precedence and scope differ in ways that matter for security pipelines.
TreatWarningsAsErrors
The standard property. When set to true, every compiler warning and analyzer warning becomes a build error:
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
Scope: Applies to the entire build — both C# compiler warnings (CS-prefixed) and Roslyn analyzer warnings (CA-prefixed, IDE-prefixed, custom rules).
Problem: Too broad for most codebases. Setting this globally turns style warnings, IDE suggestions, and documentation warnings into hard build failures alongside genuine security violations. The result is teams disabling the setting rather than fixing the noise.
BTreatWarningsAsErrors (MSBuild-level)
BTreatWarningsAsErrors is the MSBuild-level equivalent of TreatWarningsAsErrors. It applies at the MSBuild project build level (not the Roslyn compiler level), meaning it converts MSBuild task warnings into errors — not necessarily C# compiler or Roslyn analyzer warnings.
It is less commonly needed in modern .NET SDK projects where the distinction between MSBuild warnings and Roslyn warnings is mostly academic. However, it appears in some project configurations and CI pipeline outputs, which is why it shows up in build logs.
<!-- MSBuild-level: convert MSBuild task warnings to errors -->
<PropertyGroup>
<BTreatWarningsAsErrors>true</BTreatWarningsAsErrors>
</PropertyGroup>
For security analysis enforcement in .NET projects, TreatWarningsAsErrors combined with WarningsAsErrors is almost always what you want — not BTreatWarningsAsErrors alone.
WarningsAsErrors (Targeted)
The most useful property for security builds. Instead of promoting all warnings, you specify exactly which rule IDs should be treated as errors:
<PropertyGroup>
<!-- Only these specific Roslyn security rules become build errors -->
<WarningsAsErrors>CA2100;CA3001;CA3002;CA3003;CA3006;CA3007;CA3012;CA5350;CA5351;CA5369;CA5394</WarningsAsErrors>
</PropertyGroup>
This is the recommended approach for security-gated pipelines. You get hard build failures on genuine security violations without noise from style rules, nullable reference warnings, or documentation requirements.
TargetRules: Scoping Which Analyzer Rules Run
TargetRules is not a single MSBuild property — it’s a concept encompassing several mechanisms that control which Roslyn analyzer rules are active for a given build target.
Using AnalysisLevel to Scope Rules
AnalysisLevel controls which built-in .NET SDK analyzer rules are active:
<PropertyGroup>
<!-- Run all recommended security + quality rules for the latest SDK -->
<AnalysisLevel>latest-recommended</AnalysisLevel>
<!-- Or pin to a specific SDK version's rule set -->
<AnalysisLevel>8.0</AnalysisLevel>
<!-- Or enable all available rules (includes experimental) -->
<AnalysisLevel>latest-all</AnalysisLevel>
</PropertyGroup>
latest-recommended is the right choice for most security teams — it enables the current SDK’s recommended rule set without pulling in experimental rules that generate noise.
Using AnalysisMode to Tune Rule Scope
AnalysisMode controls the default severity of rules within the active analysis level:
<PropertyGroup>
<!-- Default: recommended rules only -->
<AnalysisMode>Default</AnalysisMode>
<!-- Enable all rules as suggestions (error-level controlled by WarningsAsErrors) -->
<AnalysisMode>All</AnalysisMode>
<!-- Disable all rules, then selectively enable only what you explicitly configure -->
<AnalysisMode>None</AnalysisMode>
</PropertyGroup>
For a security-focused build, combine AnalysisMode=All with WarningsAsErrors targeting specific security CA rules — this ensures security rules are active and fail builds, while non-security rules remain as warnings.
EditorConfig for Per-Rule Severity
The .editorconfig file provides the most granular control over individual rule severities:
# .editorconfig — per-rule severity configuration
[*.cs]
# Enforce SQL injection detection as a build error
dotnet_diagnostic.CA2100.severity = error
# Enforce XSS detection
dotnet_diagnostic.CA3001.severity = error
# Enforce process injection detection
dotnet_diagnostic.CA3006.severity = error
# Enforce weak cryptography detection
dotnet_diagnostic.CA5350.severity = error
dotnet_diagnostic.CA5351.severity = error
# Keep code style as a warning (not a build error)
dotnet_diagnostic.IDE0001.severity = warning
dotnet_diagnostic.IDE0002.severity = warning
# Suppress nullable warnings in generated files
[*Generated*.cs]
dotnet_diagnostic.CS8600.severity = none
dotnet_diagnostic.CS8602.severity = none
This approach is ideal when different rules need different severities across different file patterns — for example, enforcing all security rules as errors everywhere, while suppressing style warnings in auto-generated migration files.
The Complete Security Build Configuration
Here is a .csproj configuration that enforces Roslyn security rules as build errors while minimizing noise from style and quality rules:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Enable analyzers during every build -->
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<!-- Use latest recommended rules -->
<AnalysisLevel>latest-recommended</AnalysisLevel>
<!-- Promote specific security rules to build errors -->
<!-- SQL Injection, XSS, LDAP, Path Traversal, OS Command, Open Redirect -->
<!-- Regex ReDoS, Weak/Broken Crypto, Insecure Deserialization, Insecure Random -->
<WarningsAsErrors>
CA2100;
CA3001;CA3002;CA3003;CA3006;CA3007;CA3012;
CA5350;CA5351;CA5358;CA5360;
CA5369;CA5394
</WarningsAsErrors>
</PropertyGroup>
<!-- Disable analyzers only for EF design-time builds -->
<!-- This prevents dotnet ef migrations from failing on security rule violations -->
<PropertyGroup Condition="'$(DesignTimeBuild)' == 'true'">
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
</PropertyGroup>
</Project>
This configuration:
- Runs analyzers on every regular
dotnet build - Treats all major Roslyn security rules as build errors
- Automatically skips analyzer execution when
dotnet efor Visual Studio triggers a design-time build
Why TreatWarningsAsErrors=true Breaks dotnet ef
When your project has TreatWarningsAsErrors=true or specific security rules in WarningsAsErrors, running dotnet ef migrations add or dotnet ef database update often fails with:
Build FAILED.
Error CA2100: Review SQL queries for security vulnerabilities
Why this happens: The dotnet ef CLI builds your project internally (to discover the DbContext and migration history) by running a full MSBuild pipeline. This internal build triggers all Roslyn analyzers — including the security rules you’ve configured as errors.
EF migration files are auto-generated and sometimes include raw SQL, System.Random seed logic, or patterns that trigger CA rules. The EF tool doesn’t need clean security analysis to find your DbContext — but it runs analyzers anyway because it triggers a standard build.
The Fix: Pass RunAnalyzersDuringBuild=false via --
The -- separator in dotnet ef commands forwards arguments directly to MSBuild:
# Disable analyzers only for the EF tool's internal build
dotnet ef migrations add MyMigration -- /p:RunAnalyzersDuringBuild=false
dotnet ef database update -- /p:RunAnalyzersDuringBuild=false
# With explicit project paths
dotnet ef database update \
--project src/DataAccess \
--startup-project src/Api \
-- /p:RunAnalyzersDuringBuild=false
This disables Roslyn analyzer execution only for that specific EF tool build. Your main application build — and the security rules you’ve configured — are entirely unaffected.
Do not solve this by setting RunAnalyzersDuringBuild=false globally in your .csproj. That silently disables all security analysis on every build, including your CI security gate.
Conditional .csproj Approach
Alternatively, configure the suppression to apply only when DesignTimeBuild is active:
<!-- Disable analyzers only during EF/IDE design-time builds -->
<PropertyGroup Condition="'$(DesignTimeBuild)' == 'true'">
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
</PropertyGroup>
This is cleaner for teams that want the config in source control rather than having to remember the -- flag on every EF command.
CI/CD Pipeline: Security Gate + EF Migration Pattern
The correct structure separates the security-gated application build from the EF migration step:
GitHub Actions
name: .NET Security Build + Migrate
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
security-build:
name: Build with Security Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
- name: Restore
run: dotnet restore
# ✅ Security gate — TreatWarningsAsErrors scoped to CA rules
- name: Build + Roslyn Security Analysis
run: |
dotnet build --configuration Release --no-restore \
/p:RunAnalyzersDuringBuild=true \
/p:AnalysisLevel=latest-recommended \
/warnaserror:CA2100,CA3001,CA3002,CA3003,CA3006,CA3007,CA5350,CA5351,CA5369,CA5394
migrate:
name: Apply EF Migrations
runs-on: ubuntu-latest
needs: security-build # Run only after clean security build
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
- name: Install EF CLI
run: dotnet tool install --global dotnet-ef
# ✅ Analyzers suppressed only for the EF migration build step
- name: Apply Migrations
run: |
dotnet ef database update \
--project src/DataAccess \
--startup-project src/Api \
-- /p:RunAnalyzersDuringBuild=false
env:
ConnectionStrings__Default: ${{ secrets.PROD_DB_CONNECTION }}
Azure DevOps
trigger:
- main
stages:
- stage: SecurityBuild
jobs:
- job: BuildAndAnalyze
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DotNetCoreCLI@2
displayName: Restore
inputs:
command: restore
# ✅ Security-gated build — BTreatWarningsAsErrors via /warnaserror
- task: DotNetCoreCLI@2
displayName: Build with Security Analysis
inputs:
command: build
arguments: >
--configuration Release
--no-restore
/p:RunAnalyzersDuringBuild=true
/p:AnalysisLevel=latest-recommended
/warnaserror:CA2100,CA3001,CA3006,CA5350,CA5351,CA5394
- stage: Migrate
dependsOn: SecurityBuild
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: ApplyMigrations
pool:
vmImage: 'ubuntu-latest'
steps:
- script: dotnet tool install --global dotnet-ef
displayName: Install EF Tools
# ✅ Analyzers disabled only for EF migration
- script: |
dotnet ef database update \
--project src/DataAccess \
-- /p:RunAnalyzersDuringBuild=false
displayName: Apply EF Migrations
env:
ConnectionStrings__Default: $(ProdDbConnection)
Key Roslyn Security Rules to Enforce
When configuring WarningsAsErrors or BTreatWarningsAsErrors with TargetRules, these are the Roslyn security rules worth promoting to build errors:
| Rule ID | Vulnerability | CWE |
|---|---|---|
CA2100 | SQL injection in ADO.NET commands | CWE-89 |
CA3001 | XSS via HttpResponse.Write | CWE-79 |
CA3002 | LDAP injection | CWE-90 |
CA3003 | File path injection (path traversal) | CWE-22 |
CA3006 | Process command injection | CWE-78 |
CA3007 | Open redirect | CWE-601 |
CA3012 | Regex injection / ReDoS | CWE-730 |
CA5350 | Weak cryptography (3DES) | CWE-326 |
CA5351 | Broken cryptography (MD5, DES) | CWE-327 |
CA5360 | Insecure BinaryFormatter deserialization | CWE-502 |
CA5369 | XML deserialization without DTD protection | CWE-611 |
CA5394 | Insecure randomness (System.Random) | CWE-338 |
These rules are all available in the Microsoft.CodeAnalysis.NetAnalyzers package, which is included by default in .NET 5+ SDK projects.
What Roslyn Enforcement Misses
Configuring TreatWarningsAsErrors, BTreatWarningsAsErrors, and TargetRules for Roslyn security analysis is a valuable first layer. But Roslyn analyzers have hard limitations:
No interprocedural taint analysis. Roslyn CA rules detect vulnerabilities within a single method. They cannot trace SQL injection across three function calls, or XSS through a data transformation pipeline. CA2100 only fires when the SqlCommand is constructed with string concatenation in the same method as the user input. If the query building is delegated to a helper method, the rule doesn’t fire.
No second-order injection detection. If user input is stored in a database in one request and then retrieved and used unsafely in a later query, Roslyn analyzers don’t detect it. The database is an opaque boundary to Roslyn’s analysis scope.
No stored XSS detection. Roslyn can detect Response.Write(Request.QueryString["x"]) (reflected XSS in the same method), but not the pattern where user input is stored, retrieved, and then rendered without encoding across two separate controller actions.
No DAST. Roslyn analyzes source code only. Runtime vulnerabilities — authentication flaws, insecure session management, business logic errors — require Dynamic Application Security Testing against the running application.
For these cases, a SAST platform with full interprocedural taint analysis — like Offensive360 — complements Roslyn analyzers in the same CI/CD pipeline. Roslyn runs fast in the IDE and catches obvious patterns during compilation. Offensive360 runs a deeper analysis step with full data-flow tracking across your entire codebase, including second-order SQL injection and cross-file taint flows that Roslyn cannot see.
Common Configuration Mistakes
Setting TreatWarningsAsErrors=true Globally Without WarningsAsErrors Scoping
This promotes every compiler warning to a build error — including CS8600 nullable reference warnings, IDE0xxx style suggestions, and documentation warnings. Teams quickly disable it when the noise is overwhelming, eliminating the security value.
Fix: Use WarningsAsErrors to target only the security CA rules you want as errors.
Setting RunAnalyzersDuringBuild=false Globally to Fix dotnet ef
This silently disables all Roslyn security analysis on every build. The CI log shows green with no security findings — because analyzers aren’t running.
Fix: Pass -- /p:RunAnalyzersDuringBuild=false only to dotnet ef commands. Keep RunAnalyzersDuringBuild=true in your main build step.
Suppressing Specific Warnings in Migration Files Without Reviewing Them
Auto-generated migration files sometimes trigger CA2100 because they contain inline SQL for complex schema operations. The correct response is to suppress at the file level (via #pragma warning disable CA2100) or to move raw SQL to a safe parameterized helper — not to disable CA2100 globally.
Frequently Asked Questions
What is the difference between TreatWarningsAsErrors and BTreatWarningsAsErrors?
TreatWarningsAsErrors is the C#/Roslyn compiler property — it converts C# compiler warnings (CS-prefixed) and Roslyn analyzer warnings (CA-prefixed) into build errors. BTreatWarningsAsErrors is an MSBuild-level property that converts MSBuild task-level warnings into errors, which is a different layer of the build pipeline. For enforcing Roslyn security rules in .NET projects, TreatWarningsAsErrors (or the more targeted WarningsAsErrors) is the relevant property.
What does TargetRules mean in the context of .NET builds?
TargetRules isn’t a single MSBuild property — it refers to the combination of mechanisms that scope which analyzer rules are active and at what severity: AnalysisLevel, AnalysisMode, WarningsAsErrors, and .editorconfig severity directives. Together these form your “target rule set” for a given build configuration.
Can I use WarningsAsErrors per-project in a solution?
Yes. Each .csproj file has its own WarningsAsErrors configuration. In a multi-project solution, you can enforce stricter rules on security-critical projects (API layer, data access layer) while keeping more relaxed settings on infrastructure projects.
Why does dotnet ef fail when I set TreatWarningsAsErrors=true?
The EF CLI builds your project internally to find the DbContext. That build triggers all Roslyn analyzers. If any warning is treated as an error, the EF build fails. Solution: pass -- /p:RunAnalyzersDuringBuild=false to dotnet ef commands, or add <Condition="'$(DesignTimeBuild)' == 'true'"> in your .csproj to disable analyzers during design-time builds only.
Does BTreatWarningsAsErrors=true disable dotnet ef too?
If BTreatWarningsAsErrors is causing MSBuild task-level warnings to fail the EF internal build, the same fix applies: pass /p:BTreatWarningsAsErrors=false after the -- separator. However, this is less common than the TreatWarningsAsErrors/WarningsAsErrors issue.
Summary
| Property | Scope | Use For |
|---|---|---|
TreatWarningsAsErrors=true | All compiler + analyzer warnings | Strict enforcement — broad noise |
WarningsAsErrors=CA2100;CA3001... | Specific rule IDs | Targeted security enforcement |
BTreatWarningsAsErrors=true | MSBuild task warnings | MSBuild-level enforcement |
RunAnalyzersDuringBuild=true | Whether analyzers run at all | Enable for CI security gates |
AnalysisLevel=latest-recommended | Which rules are available | Latest SDK recommended rules |
AnalysisMode=All | Default severity of rules | Enable all rules, scope via WarningsAsErrors |
Recommended production configuration:
- Set
WarningsAsErrorsto the specific Roslyn security CA rules you want enforced - Set
RunAnalyzersDuringBuild=truein your main build - Disable
RunAnalyzersDuringBuildonly fordotnet efcommands via-- /p:RunAnalyzersDuringBuild=false - Use
.editorconfigfor granular per-file and per-rule severity overrides
For deeper coverage beyond what Roslyn analyzers provide, Offensive360 SAST integrates alongside your Roslyn configuration as an independent analysis step — detecting the complex injection chains and data-flow vulnerabilities that CA rules cannot see. Run a one-time scan for $500 or book a demo.