Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Roslyn Analyzers: Enforcing the Chain at Compile Time

The MyApp.Requirements.Analyzers project is a Roslyn analyzer + source generator that enforces three invariants: every requirement is specified, every specification is implemented, and every implementation is tested. Violations are compiler errors or warnings -- not runtime failures.

Analyzer 1: Requirement Coverage (REQ1xx)

Scans Layer 1 (MyApp.Requirements) for all types inheriting RequirementMetadata with abstract AC methods. Scans Layer 2 (MyApp.Specifications) for [ForRequirement] attributes. Reports gaps.

Diagnostic Severity Trigger
REQ100 Error Feature type has no [ForRequirement(typeof(Feature))] interface in Specifications
REQ101 Error Feature AC method has no matching [ForRequirement(..., nameof(AC))] on any spec interface method
REQ102 Warning Story type has no specification (acceptable for small stories, error in strict mode)
REQ103 Info Requirement fully specified -- all ACs have spec methods
// Build output:
error REQ100: UserRolesFeature has 3 acceptance criteria but no ISpec interface
              references it via [ForRequirement(typeof(UserRolesFeature))]
              → Create IUserRolesSpec with [ForRequirement(typeof(UserRolesFeature))]

error REQ101: UserRolesFeature.RoleChangeTakesEffectImmediately has no matching
              spec method with [ForRequirement(typeof(UserRolesFeature),
              nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
              → Add a method to IUserRolesSpec with the matching [ForRequirement]

Analyzer 2: Specification Implementation (REQ2xx)

Scans Layer 2 interfaces decorated with [ForRequirement]. Scans Layer 3 (MyApp.Domain) for classes implementing those interfaces. Reports unimplemented specs.

Diagnostic Severity Trigger
REQ200 Error Spec interface has no implementing class in Domain
REQ201 Warning Implementing class does not have [ForRequirement] attribute (works but loses traceability)
REQ202 Warning Implementing class has [ForRequirement] but method-level attributes are missing
REQ203 Info Spec fully implemented -- all methods have [ForRequirement] on impl
// Build output:
error REQ200: IUserRolesSpec is not implemented by any class in MyApp.Domain
              → Create a class that implements IUserRolesSpec

warning REQ201: AuthorizationService implements IUserRolesSpec but is missing
                [ForRequirement(typeof(UserRolesFeature))] on the class
                → Add [ForRequirement(typeof(UserRolesFeature))] for traceability

warning REQ202: AuthorizationService.VerifyImmediateRoleEffect implements
                IUserRolesSpec but is missing [ForRequirement(typeof(UserRolesFeature),
                nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
                → Add method-level [ForRequirement] for IDE navigation

Analyzer 3: Test Coverage (REQ3xx)

Scans Layer 4 (MyApp.Tests) for [TestsFor] and [Verifies] attributes. Cross-references with Layer 1 AC methods. Reports untested ACs.

Diagnostic Severity Trigger
REQ300 Error Feature has zero [TestsFor] test classes
REQ301 Warning AC method has no [Verifies(..., nameof(AC))] test
REQ302 Warning [Verifies] references an AC method that doesn't exist (stale test)
REQ303 Info Feature fully tested -- all ACs have at least one [Verifies] test
// Build output:
error REQ300: JwtRefreshStory has 2 acceptance criteria but no test class
              with [TestsFor(typeof(JwtRefreshStory))]

warning REQ301: UserRolesFeature.RoleChangeTakesEffectImmediately has no test
                with [Verifies(typeof(UserRolesFeature),
                nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
                → Add a test method with [Verifies] for this AC

warning REQ302: UserRolesTests.OldTest references nameof(UserRolesFeature.DeletedAC)
                which no longer exists on UserRolesFeature
                → Remove or update the stale [Verifies] attribute

Analyzer 4: Quality Gates (REQ4xx)

Integrates with a dotnet quality-gates tool (invoked as an MSBuild target after test execution) to enforce that tests don't just exist -- they pass, meet coverage thresholds, and satisfy performance budgets.

MSBuild integration:

<!-- MyApp.Tests.csproj -->
<PropertyGroup>
  <RequirementMinCoverage>80</RequirementMinCoverage>
  <RequirementMinPassRate>100</RequirementMinPassRate>
  <RequirementMaxTestDuration>5000</RequirementMaxTestDuration> <!-- ms per test -->
</PropertyGroup>

<Target Name="RequirementQualityGates" AfterTargets="VSTest">
  <Exec Command="dotnet quality-gates check
    --trx $(TestResultsDir)/*.trx
    --coverage $(CoverageResultsDir)/coverage.cobertura.xml
    --traceability $(IntermediateOutputPath)/TraceabilityMatrix.g.cs
    --min-pass-rate $(RequirementMinPassRate)
    --min-coverage $(RequirementMinCoverage)
    --max-duration $(RequirementMaxTestDuration)"
    ConsoleToMsBuild="true" />
</Target>

What dotnet quality-gates check validates:

Gate What it checks Fails when
Pass rate All [Verifies] tests passed in .trx results Any test marked [Verifies] failed
AC coverage Every AC in TraceabilityMatrix has a passing [Verifies] test AC exists but its test failed or is missing
Code coverage Lines touched by [Verifies] tests cover the [ForRequirement] methods Implementation method has <N% line coverage
Duration Individual test execution time Any [Verifies] test exceeds max duration
Flakiness Test result consistency across runs A [Verifies] test has inconsistent pass/fail across retries
Fuzz testing Auto-generated random/boundary inputs for AC methods Implementation crashes or returns unexpected results on edge cases

The fuzz testing gate deserves special attention. Because every AC is an abstract method with a typed signature, the tool knows exactly what inputs to generate. For AdminCanAssignRoles(UserId actingUser, UserId targetUser, RoleId role), it can automatically produce: null values, empty GUIDs, duplicate user/target IDs, nonexistent role IDs, extremely long strings, Unicode edge cases, and concurrent calls. The spec method's Result return type means the tool knows what "not crashing" looks like -- a Result.Failure with a reason is acceptable; an unhandled exception is not. This is property-based testing derived from the requirement signature itself.

Output:

$ dotnet quality-gates check --trx results.trx --coverage coverage.xml ...

Quality Gates Report
====================

FEATURE: UserRolesFeature (3 ACs, 5 tests)
  ✓ AdminCanAssignRoles      2 tests, 100% pass, 94% coverage, avg 12ms
    fuzz: 500 inputs, 0 crashes, 12 Result.Failure (expected)
  ✓ ViewerHasReadOnlyAccess  2 tests, 100% pass, 88% coverage, avg 8ms
    fuzz: 500 inputs, 0 crashes, 8 Result.Failure (expected)
  ✗ RoleChangeTakesEffect    1 test,  100% pass, 62% coverage ← BELOW 80% THRESHOLD
    fuzz: 500 inputs, 1 CRASH ← NullReferenceException with empty RoleId
    → AuthorizationService.VerifyImmediateRoleEffect:L45-L52 not covered

STORY: JwtRefreshStory (2 ACs, 0 tests)
  ✗ TokensExpireAfterOneHour   NO TESTS
  ✗ RefreshExtendsBySevenDays  NO TESTS

Summary: 2/3 quality gates passed for UserRolesFeature (fuzz found 1 crash)
         0/2 quality gates passed for JwtRefreshStory
         Overall: FAIL (exit code 1)

Severity Configuration

The analyzer severity is configurable per-project via .editorconfig:

# .editorconfig
[*.cs]

# Requirements → Specifications: treat missing specs as errors
dotnet_diagnostic.REQ100.severity = error
dotnet_diagnostic.REQ101.severity = error
dotnet_diagnostic.REQ102.severity = warning

# Specifications → Implementation: treat missing impl as errors
dotnet_diagnostic.REQ200.severity = error
dotnet_diagnostic.REQ201.severity = warning
dotnet_diagnostic.REQ202.severity = suggestion

# Implementation → Tests: treat missing tests as warnings (errors in CI)
dotnet_diagnostic.REQ300.severity = warning
dotnet_diagnostic.REQ301.severity = warning
dotnet_diagnostic.REQ302.severity = error    # stale tests are always errors

For CI, override to strict:

<!-- Directory.Build.props -->
<PropertyGroup Condition="'$(CI)' == 'true'">
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <WarningsAsErrors>REQ100;REQ101;REQ200;REQ300;REQ301</WarningsAsErrors>
</PropertyGroup>

The Full CI Pipeline

# .github/workflows/build.yml
jobs:
  build:
    steps:
      - run: dotnet build --warnaserror     # REQ1xx, REQ2xx, REQ3xx analyzers
      - run: dotnet test --collect:"XPlat Code Coverage" --logger:trx
      - run: dotnet quality-gates check     # REQ4xx quality gates
        --trx TestResults/*.trx
        --coverage TestResults/**/coverage.cobertura.xml
        --min-pass-rate 100
        --min-coverage 80

Three enforcement points, one build:

  1. dotnet build -- Roslyn analyzers catch structural gaps (missing specs, missing implementations, missing tests)
  2. dotnet test -- tests execute against the implementation
  3. dotnet quality-gates -- post-test analysis ensures tests are passing, coverage is sufficient, and performance is acceptable

If any gate fails, the build fails. The chain is enforced from requirement to quality-verified production code.

⬇ Download