Roslyn Analyzers & Code Quality Rules
Granit.Analyzers provides Roslyn analyzers that enforce Granit conventions at compile time, with immediate feedback in the IDE (red squiggles). They complement runtime checks and architecture tests with zero-cost static analysis.
Installation
Section titled “Installation”dotnet add package Granit.AnalyzersThe package ships both analyzers and code fix providers. No additional configuration is required — rules activate automatically based on the project’s dependencies.
Architecture
Section titled “Architecture”Rules enforcing modular monolith boundaries.
| Rule | Severity | CodeFix | Description |
|---|---|---|---|
| GRMOD001 | Error | Yes | Cross-module reference to internal type — use Contracts |
GRMOD001 — Cross-module reference detection
Section titled “GRMOD001 — Cross-module reference detection”Detects when code in one module references internal types from another module
instead of using the .Contracts namespace.
Applies to: projects using the *.Modules.{ModuleName} namespace convention.
Opt-in: the analyzer only activates when *.Modules.* namespaces are present
in the compilation. Projects without this convention pay zero runtime cost.
Allowed references:
- Types in
*.Modules.{Module}.Contractsand sub-namespaces - Types within the same module
- Types outside
*.Modules.*(framework, BCL, shared libraries)
Forbidden references:
- Types in
*.Modules.{OtherModule}.Domain - Types in
*.Modules.{OtherModule}.Handlers - Types in
*.Modules.{OtherModule}.Services - Any non-Contracts namespace of another module
CodeFix: if a type with the same name exists in the target module’s .Contracts
namespace, the code fix suggests replacing the using directive.
// ❌ GRMOD001: InventoryService from module Inventory cannot be// referenced from module Ordersusing MyApp.Modules.Inventory.Services;
namespace MyApp.Modules.Orders.Handlers;
public class OrderHandler(InventoryService inventory) { }// ✅ Use the Contracts namespaceusing MyApp.Modules.Inventory.Contracts;
namespace MyApp.Modules.Orders.Handlers;
public class OrderHandler(IInventoryReader inventory) { }Migrations
Section titled “Migrations”Rules enforcing zero-downtime migration patterns (expand/migrate/contract).
| Rule | Severity | CodeFix | Description |
|---|---|---|---|
| GRMIGA001 | Error | — | DropColumn requires a Contract-phase annotation |
| GRMIGA002 | Error | — | RenameColumn is not zero-downtime safe |
| GRMIGA003 | Warning | — | AddColumn NOT NULL without default risks table lock |
| GRMIGA004 | Warning | — | AlterColumn type change requires Contract-phase annotation |
Security
Section titled “Security”Rules enforcing testability, secret management, and GDPR compliance.
| Rule | Severity | CodeFix | Description |
|---|---|---|---|
| GRSEC001 | Warning | Yes | Avoid DateTime.Now/UtcNow — use IClock |
| GRSEC002 | Warning | Yes | Avoid Guid.NewGuid() — use IGuidGenerator |
| GRSEC003 | Error | — | Potential hardcoded secret detected |
| GRSEC004 | Warning | Yes | Avoid direct IResponseCookies — use IGranitCookieManager |
| GRSEC010 | Error | — | PII-indicative name used as metric tag key |
| GRSEC011 | Error | — | PII-indicative placeholder in LoggerMessage template |
GRSEC010 — PII in metric tag keys
Section titled “GRSEC010 — PII in metric tag keys”Detects TagList initializers where the key (first string literal in each
{ "key", value } pair) matches a PII-indicative pattern: email, phone,
ipAddress, username, ssn, passport, token, etc.
PII as metric tag keys causes both a GDPR violation (PII in telemetry backends) and a cardinality explosion (unbounded memory, high cost in Prometheus/Grafana).
// ❌ GRSEC010: Tag key 'email' matches a PII pattern_counter.Add(1, new TagList{ { "email", user.Email }, { "tenant_id", tenantId },});// ✅ Use bounded, low-cardinality identifiers_counter.Add(1, new TagList{ { "user_id", userId }, { "tenant_id", tenantId },});GRSEC011 — PII in LoggerMessage placeholders
Section titled “GRSEC011 — PII in LoggerMessage placeholders”Detects [LoggerMessage] templates containing placeholders whose names match
PII-indicative patterns. Supports both named (Message = "...") and positional
([LoggerMessage(id, Level, "...")]) constructor forms. Escaped braces ({{/}})
are correctly skipped.
PII in structured logs violates GDPR Art. 5 (data minimization) and exposes personal data to log aggregation systems (ELK, Loki, Application Insights).
// ❌ GRSEC011: PII placeholder {Email}[LoggerMessage(Level = LogLevel.Information, Message = "User {Email} logged in from {IpAddress}")]private static partial void LogUserLogin( ILogger logger, string email, string ipAddress);// ✅ Log identifiers, not PII[LoggerMessage(Level = LogLevel.Information, Message = "User {UserId} logged in")]private static partial void LogUserLogin( ILogger logger, string userId);Entity Framework
Section titled “Entity Framework”Rules enforcing async-first EF Core patterns.
| Rule | Severity | CodeFix | Description |
|---|---|---|---|
| GREF001 | Warning | Yes | Use SaveChangesAsync() instead of SaveChanges() |
Rules enforcing Minimal API conventions and RFC 7807 compliance.
| Rule | Severity | CodeFix | Description |
|---|---|---|---|
| GRAPI001 | Warning | Yes | Use TypedResults instead of Results for OpenAPI |
| GRAPI002 | Warning | Yes | Use TypedResults.Problem() instead of BadRequest (RFC 7807) |
Suppressing rules
Section titled “Suppressing rules”To suppress a specific rule for a file or project:
// File-level suppression#pragma warning disable GRMOD001<!-- Project-level suppression in .csproj --><PropertyGroup> <NoWarn>$(NoWarn);GRMOD001</NoWarn></PropertyGroup>