Documentation

adrs/004-roslyn-analyzer-enforcement.md

ADR-004: Roslyn Analyzer for Architectural Rule Enforcement

Status

Accepted

Context

The Polylith architecture depends on a critical rule: components must only reference other components' interfaces (.Spec projects), never their implementations. This rule ensures:

  • Loose coupling: Components remain decoupled at compile time
  • Modularity: Components can be swapped without recompilation
  • Runtime flexibility: Dynamic component loading works correctly
  • Clear boundaries: Architectural intent is enforced

However, enforcing this rule manually is error-prone and relies on:

  • Developer discipline: Remembering not to add forbidden references
  • Code review process: Catching violations during peer review
  • Runtime failures: Discovering violations when components fail to load

The challenge was how to enforce this architectural rule automatically and catch violations at compile time, not runtime.

Several enforcement approaches were considered:

  1. Manual code reviews: Rely on human review process
  2. Build scripts: Custom PowerShell/bash scripts to validate references
  3. MSBuild tasks: Custom MSBuild tasks to check project references
  4. Unit tests: Reflection-based tests to verify assembly references
  5. Roslyn analyzers: Compile-time code analysis with real-time feedback

Decision

We implemented a custom Roslyn analyzer (SpecReferenceRequirementAnalyzer) that enforces the architectural rule at compile time.

Analyzer Rule: ACSIS0001

  • Severity: Error (build-breaking)
  • Scope: All Acsis assemblies
  • Rule: Only .Spec assemblies may be referenced across components

Implementation Details:

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class SpecReferenceRequirementAnalyzer : DiagnosticAnalyzer

Detection Logic:

  1. Analyze all code operations (method calls, object creation, property access)
  2. Check if the target member is from an Acsis assembly
  3. Verify the target assembly ends with .Spec
  4. Report violation if non-.Spec assembly is referenced

Integration:

  • Included in all component and base projects via Directory.Build.props
  • Runs during every build and in IDE real-time
  • Provides immediate feedback to developers

Consequences

Positive

Architectural Integrity:

  • Compile-time enforcement: Violations caught before runtime
  • Automatic validation: No manual checking required
  • Consistent application: Rule applied uniformly across all projects
  • Early feedback: Developers get immediate notification of violations

Developer Experience:

  • IDE integration: Real-time squiggles and error highlighting
  • Clear error messages: Descriptive messages explain the violation
  • Immediate fix guidance: Error points to exactly what needs to change
  • Build integration: Prevents building code with architectural violations

Process Benefits:

  • Reduced review burden: Automated checking reduces manual review needs
  • Documentation: The analyzer serves as executable documentation of the rule
  • Onboarding: New developers learn the rule through immediate feedback
  • Consistency: Eliminates subjective interpretation of the architectural rule

Quality Assurance:

  • Runtime reliability: Prevents component loading failures
  • Maintainability: Enforces loose coupling that improves maintainability
  • Refactoring safety: Violations surface immediately during refactoring
  • Deployment confidence: Architectural violations can't reach production

Negative

Development Overhead:

  • Analyzer complexity: Custom analyzer adds codebase complexity
  • Maintenance burden: Analyzer itself needs to be maintained and updated
  • Performance impact: Analysis adds slight overhead to build times
  • Learning curve: Developers need to understand analyzer rules and messages

Rigidity Concerns:

  • Edge cases: May flag legitimate cases that should be exceptions
  • Innovation blocking: Might prevent valid architectural experiments
  • Migration challenges: Could complicate migration from legacy code
  • False positives: Analyzer might incorrectly flag valid code patterns

Technical Limitations:

  • Reflection gaps: May not catch all forms of dynamic references
  • Runtime binding: Can't analyze reflection-based component access
  • Build tool dependency: Adds dependency on Roslyn analyzer infrastructure
  • Version compatibility: Must be maintained for .NET version compatibility

Debugging Complexity:

  • Analyzer debugging: Troubleshooting analyzer issues is specialized skill
  • Build failures: Cryptic analyzer failures can be difficult to diagnose
  • IDE integration: Inconsistent behavior across different IDEs
  • Suppression complexity: Properly suppressing legitimate violations

Mitigation Strategies

Handling Edge Cases:

// Suppress for legitimate violations (sparingly used)
#pragma warning disable ACSIS0001
using Acsis.Dynaplex.Engines.CoreData; // Justified reason documented
#pragma warning restore ACSIS0001

Clear Error Messages:

private const string MessageFormat =
    "Assembly '{0}' references assembly '{1}'. Only specification assemblies (.Spec) may be referenced across bricks.";

Comprehensive Testing:

// Test analyzer with various scenarios
[Test]
public void Should_Allow_Spec_References() { /* ... */ }

[Test]
public void Should_Reject_Implementation_References() { /* ... */ }

Documentation and Training:

  • Clear documentation of when suppressions are appropriate
  • Training materials explaining the architectural rule
  • Examples of correct and incorrect reference patterns

Implementation Details

Core Analysis Logic:

private static void StartAnalysis(CompilationStartAnalysisContext ctx) {
    var referencingAsmName = ctx.Compilation.Assembly.Name;

    ctx.RegisterOperationAction(
        opCtx => {
            var referencedAssembly = GetAssembly(opCtx.Operation);

            if (referencedAssembly?.Name?.StartsWith("Acsis.") == true &&
                !referencedAssembly.Name.EndsWith(".Spec")) {

                opCtx.ReportDiagnostic(
                    Diagnostic.Create(_forbiddenRefRule,
                        opCtx.Operation.Syntax.GetLocation(),
                        referencingAsmName,
                        referencedAssembly.Name));
            }
        },
        OperationKind.Invocation,
        OperationKind.ObjectCreation,
        OperationKind.FieldReference,
        OperationKind.PropertyReference
    );
}

Project Integration:

<!-- In component projects -->
<ItemGroup>
    <ProjectReference Include="$(Analyzers)"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false"/>
</ItemGroup>

Error Example:

Error ACSIS0001: Assembly 'Acsis.Foundation' references assembly 'Acsis.Dynaplex.Engines.CoreData'.
Only specification assemblies (.Spec) may be referenced across bricks.

Effectiveness Metrics

Since implementation, the analyzer has:

  • Prevented violations: Caught numerous accidental implementation references
  • Guided development: Helped developers understand the architectural pattern
  • Improved reliability: Eliminated runtime component loading failures due to reference violations
  • Reduced review time: Automated checking reduced manual code review burden
  • ADR-001: Architecture requiring reference restrictions
  • ADR-002: .Spec naming that analyzer detects
  • ADR-003: Component loading that requires clean references

Alternatives Considered

MSBuild Tasks

Pros: Integrated into build process, could check project references
Cons: Only catches project-level references, not code-level usage

Unit Tests

Pros: Simple to implement, flexible validation logic
Cons: Run too late (after code is written), not real-time feedback

Build Scripts

Pros: Simple scripting, easy to customize
Cons: Platform-specific, not integrated with IDE, poor developer experience

FxCop/Code Analysis Rules

Pros: Mature framework, established patterns
Cons: Being deprecated in favor of Roslyn analyzers, limited flexibility

Future Enhancements

  1. Additional Rules: Analyzers for other architectural patterns
  2. Configuration: Allow configuration of allowed/forbidden patterns
  3. Metrics: Collect metrics on architectural violations over time
  4. IDE Extensions: Enhanced IDE integration with quick fixes
  5. Documentation Generation: Generate architectural documentation from analyzer rules

The Roslyn analyzer has proven to be highly effective at maintaining architectural integrity while providing excellent developer experience through real-time feedback.