Skip to content

Code analysis rules

Arc’s Core Roslyn analyzers check command, query, validation, and concept declarations at compile time. They do not require Chronicle. The optional Chronicle integration rules have a separate ARCCHR prefix.

These are the active ARC descriptors in the current source, all enabled by default in category Arc. Severity is the default before your project’s analyzer configuration. The release-tracking file currently lists ARC0001–ARC0018 under Unshipped; its Shipped table has no rule entries. This inventory describes the current source, not a claim that every older NuGet version contains every rule.

Rule IDDescriptor titleSeverityAnalyzer
ARC0001Incorrect Query method signature on ReadModelErrorReadModelAnalyzer
ARC0002Missing [Command] attribute on command-like typeWarningCommandAnalyzer
ARC0003Handle() must be on [Command] typeErrorCommandAnalyzer
ARC0004[Command] type must have public Handle() methodErrorCommandAnalyzer
ARC0005Value produced by Provide is not consumed by HandleWarningCommandProvideAnalyzer
ARC0006Command-scoped read model can be missingWarningInjectedReadModelAnalyzer
ARC0007Command should be declared as a recordWarningModelBoundRecordAnalyzer
ARC0008ReadModel should be declared as a recordWarningModelBoundRecordAnalyzer
ARC0009Concept should be declared as a recordWarningConceptRecordAnalyzer
ARC0010Command Handle() wraps a synchronous result in a TaskWarningCommandHandleTaskWrappingAnalyzer
ARC0011[Roles] argument should use nameof instead of a string literalWarningRolesLiteralAnalyzer
ARC0012Arc artifact throws a built-in exception typeWarningArcArtifactBuiltInExceptionAnalyzer
ARC0013Validator rule dereferences a possibly-null concept memberWarningValidatorConceptDereferenceAnalyzer
ARC0014Generic query method on ReadModelErrorReadModelAnalyzer
ARC0015Query parameter converted to a concept in the method bodyWarningQueryParameterConceptTypeAnalyzer
ARC0016Invalid command operation methodErrorCommandOperationAnalyzer
ARC0017Use CommandOperations for operation batchesErrorCommandOperationAnalyzer
ARC0018Operation cannot have a generated invokerErrorCommandOperationAnalyzer

The individual rule pages contain deliberately invalid diagnostic examples, not runnable application checkpoints. Compile each alternative separately; duplicate domain type names are intentional.

Reports a non-record type marked [Command]. Declare the command as a record and retain its public instance Handle() method. This is a modeling warning, not a claim that the runtime rejects every class-based command.

Reports a non-record type marked [ReadModel]. Prefer a record to represent returned state with value equality. A standalone Arc read model need not be an event-sourced projection.

Reports a non-record class deriving from Cratis.Concepts.ConceptAs<T>, including indirect inheritance. Use a record. Since ConceptAs<T> is itself a record, an ordinary derived class also violates the C# record inheritance rules; this diagnostic can accompany a compiler error.

Reports a public instance Handle() on a [Command] that returns Task or Task<T> and either:

  • is async but has no own await, or
  • returns only Task.FromResult(...) / Task.CompletedTask wrappers.

Return the synchronous value (or void) directly. Forwarding a genuine asynchronous operation is not the same as wrapping a synchronous value. Ordinary response DTOs do not need Chronicle. The editor offers Unwrap to synchronous Handle(); an async example without await may also produce compiler warning CS1998.

Reports direct string-literal arguments to Arc’s [Roles] attribute. Prefer nameof(ApplicationRole.Administrator) when that enum member defines the role’s actual wire name. Do not change an externally assigned role string merely to silence the analyzer.

The editor offers Use nameof for role only when it finds a matching enum member. The current fix searches by member name and inserts an unqualified enum type name; review the selected enum and its namespace, especially when several enums share a member name. It does not prove that your identity provider issues that role.

Reports explicit throw new ... of an exception in System or a System.* namespace from:

  • a [Command] type’s Handle() method,
  • a CommandValidator<T> or ConceptValidator<T> type,
  • a Chronicle IReactor implementation, if the optional integration is present.

Use validation rejection for expected invalid input. For genuinely exceptional failures, use a domain-named exception. Simply renaming an exception does not turn it into a validation result. For expected invalid input, return an explicit validation result rather than throwing an ordinary domain exception.

ARC0013: Concept dereferences in validators

Section titled “ARC0013: Concept dereferences in validators”

Reports a FluentValidation RuleFor selector that dereferences a member of a concept property, such as RuleFor(command => command.Name.Value). Input can contain a null concept even when its declaration is non-nullable. Prefer validating the concept itself and placing its invariant in a ConceptValidator<T>; use a null guard when checking an optional concept’s member.

Current limitation: detection is syntactic after identifying the concept type. It does not analyze a surrounding .When(...), rule ordering, or cascade settings. A correctly guarded member selector can still be reported. Review the guard before applying a narrowly scoped suppression; do not remove required validation to obtain a clean build.

Reports public or internal static generic methods on a [ReadModel] whose return type has an accepted query shape. Query invocation cannot close the method’s type parameters. Make the query non-generic, or move a generic composition helper off the read model. A valid return type alone does not make a generic method invocable.

Reports string, Guid, or nullable Guid parameters converted to a ConceptAs<T>-derived type inside a public or internal static non-void method on a [ReadModel]. The analyzer recognizes conversion operations (including implicit conversions), not every possible manual construction or data-flow pattern.

Declare the parameter as the concept when you want Arc’s query pipeline to validate that concept before invoking the method. Preserve null-aware handling for omitted input. For free-text search whose accepted values intentionally differ from an identifier concept’s rules, review the model or suppress this warning locally with a reason. Direct static calls still bypass the query pipeline.

Reports an ICommandOperation implementation without exactly one valid public instance Execute() or with an invalid optional Compensate(). Both methods must be nongeneric and return void, Task, or ValueTask. Unsupported shapes include async void, value-returning tasks, static or overloaded methods, by-reference arguments, optional service parameters, and service locators. CommandOperationFailure can be requested only by Compensate().

Use the operation declaration contract. Forward service calls directly; do not add a separate executor type or application try/catch merely to satisfy the convention.

Reports a command return shape containing a bare collection of operation values, including arrays and typed enumerables inside supported wrappers. Return CommandOperations instead. The explicit immutable batch distinguishes server execution from an ordinary collection response and supports collection expressions such as [].

See zero-to-many operations. Do not work around the diagnostic by erasing operation types to object; use a meaningful declared return contract.

Reports a concrete operation whose type cannot be referenced by its generated invoker. Use a public or internal nongeneric operation in accessible nongeneric containing types. File-local, private nested, or generic declarations do not provide the supported generated invocation shape. A file-local type cannot be referenced from the separate generated source file.

Runtime validation remains necessary when declarations are loaded without the source generator. Generated operation invokers do not establish NativeAOT support for every other Arc execution path.

Only these Core diagnostics currently have code-fix providers. The other rules require manual changes.

RuleEditor actionProvider
ARC0010Unwrap to synchronous Handle()UnwrapCommandHandleTaskCodeFixProvider
ARC0011Use nameof for roleUseNameofForRolesCodeFixProvider

Both providers expose Roslyn’s batch Fix All support. Review and compile the result, particularly the enum lookup for ARC0011.

The Cratis.Arc.Core NuGet package depends on Cratis.Arc.Core.CodeAnalysis. Install Core normally; no Chronicle package is needed:

Terminal window
dotnet add package Cratis.Arc.Core

The analyzer package places Cratis.Arc.Core.CodeAnalysis.dll and Cratis.Arc.Core.CodeAnalysis.CodeFixes.dll under analyzers/dotnet/cs. Compiler analysis and workspace-based editor fixes live in separate assemblies. Arc Core’s source generators are separate assets, not additional ARC rules. Editor support and the installed package version determine which fixes you see.

For source contributors, the inventory is defined by Source/DotNET/Arc.Core.CodeAnalysis/DiagnosticDescriptors.cs, each analyzer’s SupportedDiagnostics, AnalyzerReleases.*.md, and Arc.Core.CodeAnalysis.Package/Arc.Core.CodeAnalysis.Package.csproj. Check all four when adding a rule; a descriptor alone does not demonstrate that a diagnostic runs or is packaged.