Skip to content

Diagnostics

Pragmatic Design emits compile-time diagnostics prefixed with PRAG to guide correct usage of its attributes, detect configuration issues, and surface framework misuse early. Diagnostics are produced by the unified Source Generator and the module Analyzers.

All diagnostics are visible in the IDE (Rider, VS, VS Code) and in dotnet build output.

  • Error — compilation fails; the construct is invalid or will not work at runtime.
  • Warning — compilation succeeds; the construct is likely wrong or will behave surprisingly.
  • Info — hint; the construct is unusual but not incorrect.

Each module owns a dedicated PRAG range. To look up the exact title and message format for a specific code, open the corresponding Diagnostics.cs file linked below.

RangeModuleSource
PRAG0001-0099ResultResult.Analyzers/DiagnosticDescriptors.cs
PRAG0100-0199EnsurePragmatic.Ensure/src/
PRAG0200-0299Validation (0200–0206, 0209)Features/Validation/Diagnostics/
PRAG0300-0399Mapping (0300–0325)Features/Mapping/Diagnostics/MappingDiagnostics.cs
PRAG0400-0449Actions (0400–0415)Features/Actions/Diagnostics/ActionsDiagnostics.cs
PRAG0500-0599Endpoints (0500–0515, 0550–0551)Features/Endpoints/Diagnostics/EndpointsDiagnostics.cs
PRAG0600-0699Persistence (0600–0622, 0650–0651)Features/Persistence/Diagnostics/PersistenceDiagnostics.cs
PRAG0700-0716Persistence — query pipeline (0705, 0710, 0711, 0716)Features/Persistence/Diagnostics/QueryPipelineDiagnostics.cs
PRAG0800-0831Messaging (0800–0818, 0830–0831)Features/Messaging/Diagnostics/MessagingDiagnostics.cs
PRAG1000-1099Identity (1000–1003); Composition emits 1050Features/Identity/Diagnostics/IdentityDiagnostics.cs
PRAG1100-1199Ownership / Scoped (1100, 1102, 1104)Features/Persistence/Diagnostics/OwnershipDiagnostics.cs
PRAG1600-1696Composition (1600–1696, incl. 1640–1647, 1651–1652, 1685–1696 RemoteBoundary)Features/Composition/Diagnostics/CompositionDiagnostics.cs
PRAG1700-1799Caching (1700–1704, 1750–1751)Features/Caching/Diagnostics/CachingDiagnostics.cs
PRAG1800-1899Internationalization (1800–1803)Features/I18n/Diagnostics/I18NDiagnostics.cs
PRAG1900-1902PatchFeatures/Patch/Diagnostics/PatchDiagnostics.cs
PRAG2000-2050Configuration (2000, 2001, 2050)Features/Configuration/Diagnostics/ConfigurationDiagnostics.cs
PRAG2500-2549Jobs (2500–2506)Features/Jobs/Diagnostics/JobsDiagnostics.cs
PRAG2600-2601Traits ([HasComments]/[HasTags]/[HasNotes]/[HasAttachments])Features/Traits/Diagnostics/TraitDiagnostics.cs
PRAG2602-2605Resource ([Resource])Features/Resource/Diagnostics/ResourceDiagnostics.cs
PRAG2700-2702ValueObject ([ValueObject])Features/ValueObject/Diagnostics/ValueObjectDiagnostics.cs
PRAG9001Manifest generation failureFeatures/Manifest/ManifestFeature.cs

The diagnostics below are the ones new users hit most often. Full titles and messages come straight from the descriptor source files.

Action class must be partial — “Action class ‘{0}’ must be declared as partial”.

The Source Generator emits the invoker inside the same class as a partial declaration. A non-partial action class cannot receive the generated code.

// ❌ PRAG0400
[DomainAction]
public class PlaceOrder : IDomainAction<OrderResult> { ... }
// ✅
[DomainAction]
public partial class PlaceOrder : IDomainAction<OrderResult> { ... }

Entity instantiated with new bypasses the generated factory.

[Entity<TKey>] generates a Create(...) static factory that populates PersistenceId, audit fields, and raises domain events. Using new Entity() directly skips that path.

// ❌ PRAG0680
var order = new Order { ... };
// ✅
var order = Order.Create(...);

Boundary not found — a [BelongsTo<TBoundary>] or [Module<TModule>] reference points to a type that does not exist or is not a [Boundary] / [Module].

Lifetime mismatch — a scoped or transient dependency is injected into a singleton. The captured instance will be pinned for the lifetime of the app.

Optional injection[Inject] defaults to Required = false, so a missing service is injected as null and may throw a NullReferenceException at first use.

[Inject] opts into optional resolution by default. The analyzer surfaces this so the nullable contract is visible; set Required = true to fail fast at startup, or keep Required = false explicitly to acknowledge it.

// ❌ PRAG1647 — silent null if the service is unregistered
[Inject]
public IClock? Clock { get; set; }
// ✅ fail fast at startup
[Inject(Required = true)]
public IClock Clock { get; set; } = null!;

Handler pipeline, outbox, saga, topology, and request/response diagnostics. Common codes:

  • PRAG0801 — Handler class must be partial
  • PRAG0810 — Saga state property missing
  • PRAG0815[CompensateWith<T>] target must implement ICompensator<TState>
  • PRAG0830, PRAG0831 — request/response handler diagnostics (the highest Messaging codes)
  • PRAG2500[Job] class must implement IJob or IJob<T>
  • PRAG2501[RecurringJob] has an invalid cron expression
  • PRAG2502 — Job class must be partial
  • PRAG2503 — Duplicate recurring job ID
  • PRAG2504[Retry] with MaxAttempts <= 0
  • PRAG2505[Continuation<T>] target must implement IJob
  • PRAG2506 — additional Jobs diagnostic (see source)
  • PRAG2600, PRAG2601 — trait wiring diagnostics for [HasComments] / [HasTags] / [HasNotes] / [HasAttachments]. PRAG2601 warns when a trait is applied without a [Resource] on the parent — the trait entity and actions are still generated, but no endpoints are, because the route segment comes from [Resource].
  • PRAG2602[Resource] segment must be kebab-case
  • PRAG2603 — duplicate resource segment within the same boundary
  • PRAG2605Capabilities set without Read (a Read capability is recommended)
  • PRAG2700[ValueObject] record must be partial
  • PRAG2701[ValueObject] should declare a Validate(...) method so Create(...) can be generated
  • PRAG2000, PRAG2001, PRAG2050[Configuration] options-binding diagnostics.

Emitted when the inline manifest generator (run from EndpointsFeature) fails to build the API manifest for an assembly. The build still succeeds; OpenAPI/client-generation enrichment for that assembly is skipped.

All diagnostics can be suppressed via standard C# mechanisms:

<!-- Project-wide: .csproj -->
<PropertyGroup>
<NoWarn>$(NoWarn);PRAG0402</NoWarn>
</PropertyGroup>
// Per-file or per-line:
#pragma warning disable PRAG0402 // Action has no injectable dependencies
public partial class UtilityAction : VoidDomainAction { ... }
#pragma warning restore PRAG0402

Prefer a targeted suppression with a comment explaining why — the diagnostics are opinionated guardrails, and suppressing one is usually a signal that the framework assumption doesn’t fit your case.

New diagnostics must pick an ID inside an unused range of their module and follow the convention:

  1. Descriptor in {Module}Diagnostics.cs with Category = "Pragmatic.{Module}"
  2. Title as a one-line imperative sentence
  3. MessageFormat with positional {0}/{1} placeholders (avoid string interpolation)
  4. Severity: Error only if the generated code won’t work; Warning for “will work but surprising”; Info for hints
  5. A dedicated test case in the module’s Generator/ or Analyzers/ test project

See docs/completed/naming-sg.md for the full SG naming and convention playbook.