Strict Analyzers and Code Style — Enforcing Consistency Across the Solution
How to configure Roslyn analyzers, .editorconfig, TreatWarningsAsErrors, and nullable reference types in a Clean Architecture .NET project to enforce code quality and consistency automatically.
Why Automated Code Style Enforcement
Code reviews that focus on formatting and style are wasted engineering time. Automated tools enforce consistency faster and with no disagreement. The goal: reviewers focus on logic and architecture; tools handle everything else.
Production issue I've seen: A team had no nullable reference type enforcement. Over 18 months,
NullReferenceExceptionwas the #1 exception type in their error monitoring system. Every one of those crashes could have been a compile-time error. Enabling<Nullable>enable</Nullable>and<TreatWarningsAsErrors>true</TreatWarningsAsErrors>across the solution would have surfaced all of them at build time.
TreatWarningsAsErrors in Every Project
<!-- Applied to ALL project files in the solution -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>Apply this at the Directory.Build.props level so it applies automatically to every project:
<!-- Directory.Build.props (solution root) -->
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
</Project>Nullable Reference Types
// Without nullable enabled — the compiler cannot help you
public Patient? GetPatient(Guid id)
{
// Could return null — caller has no idea
}
// Caller:
var patient = GetPatient(id);
var name = patient.Name; // NullReferenceException — no warning
// With nullable enabled — everything is explicit
public Patient? GetByIdAsync(PatientId id, CancellationToken ct); // ? = may be null
// Caller:
var patient = await GetByIdAsync(id, ct);
var name = patient.Name; // CS8602: Dereference of a possibly null reference
// Fix: check first
if (patient is null)
return Result.Failure<PatientResponse>(PatientErrors.NotFound);
var name = patient.Name; // safe — compiler knows patient is non-null here.editorconfig
# .editorconfig (solution root)
root = true
[*.cs]
# Indentation
indent_style = space
indent_size = 4
tab_width = 4
# New lines
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
# C# formatting
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_indent_case_contents = true
csharp_space_after_cast = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_preserve_single_line_statements = false
# Naming rules
dotnet_naming_rule.private_fields_should_be_camel.symbols = private_fields
dotnet_naming_rule.private_fields_should_be_camel.style = camel_underscore_prefix
dotnet_naming_rule.private_fields_should_be_camel.severity = error
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.camel_underscore_prefix.capitalization = camel_case
dotnet_naming_style.camel_underscore_prefix.required_prefix = _
# Code quality
dotnet_analyzer_diagnostic.category-Performance.severity = error
dotnet_analyzer_diagnostic.category-Reliability.severity = error
# Using directives
dotnet_sort_system_directives_first = true
csharp_using_directive_placement = outside_namespace
# Prefer var
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestionRoslyn Analyzers
<!-- Directory.Build.props -->
<ItemGroup>
<!-- Microsoft's recommended analyzers -->
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.*" PrivateAssets="all" />
<!-- Async code correctness -->
<PackageReference Include="AsyncFixer" Version="1.*" PrivateAssets="all" />
<!-- Security analysis -->
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.*" PrivateAssets="all" />
</ItemGroup>Common analyzer rules enabled:
CA1822: Mark members as static when possible
CA2016: Forward CancellationToken when available
CA1062: Validate parameter before use (avoids NullReferenceException)
CA1031: Do not catch general Exception types
CS8600: Converting null literal or possible null value
CS8601: Possible null reference assignment
CS8602: Dereference of a possibly null reference
ASYNC001: Async methods should end in AsyncSuppressing False Positives
When an analyzer warning is a false positive, suppress it with context and reason:
// ✓ Suppression with explanation
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex) when (ex is not OperationCanceledException)
{
// Intentional broad catch in middleware — all unexpected failures logged and 500 returned
_logger.LogError(ex, "Unhandled exception");
await WriteErrorResponseAsync(context);
}
#pragma warning restore CA1031PRO TIP: Never use
[SuppressMessage]without aJustificationparameter. A suppression with no justification is a code smell that future developers will not know whether to trust or fix.
[SuppressMessage(
"Reliability",
"CA2007:Consider calling ConfigureAwait on the awaited task",
Justification = "ASP.NET Core does not use a SynchronizationContext")]File-Scoped Namespaces
Enforce the modern C# 10 file-scoped namespace style:
# .editorconfig
csharp_style_namespace_declarations = file_scoped:error// Old style (warning with above setting)
namespace SystemForge.Application.Patients.Commands.CreatePatient
{
public sealed class CreatePatientCommand { ... }
}
// New style (preferred)
namespace SystemForge.Application.Patients.Commands.CreatePatient;
public sealed class CreatePatientCommand { ... }EnforceCodeStyleInBuild
<!-- Directory.Build.props -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>This makes .editorconfig rules fail the build if violated — not just show warnings in the IDE. Without this setting, style rules are advisory only.
CI Verification
# .github/workflows/ci.yml
- name: Build with strict settings
run: dotnet build --configuration Release
# TreatWarningsAsErrors + EnforceCodeStyleInBuild means any style or quality
# violation fails this step and blocks the PR from mergingKey Takeaway
Strict analyzers and code style enforcement are not bureaucracy — they are automation. Every NullReferenceException that Nullable catches at compile time is a production incident that does not happen. Every naming convention enforced by
.editorconfigis a code review comment that does not need to be written. The initial setup takes a few hours; the compounding benefit over a year of development is hundreds of hours of debugging and review time saved.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.