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–ARC0028 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
ARC0019[AllowAnonymous] conflicts with [Authorize] or [Roles] on the same declarationWarningAuthorizationAttributeAnalyzer
ARC0020ASP.NET Core authorization attribute is not enforced without Arc’s ASP.NET Core integrationWarningAuthorizationAttributeAnalyzer
ARC0021Authentication scheme requires the ASP.NET Core hostWarningAuthorizationAttributeAnalyzer
ARC0022Invalid generated value parameter typeErrorGeneratedValueAnalyzer
ARC0023Generated value declared outside a command Handle methodErrorGeneratedValueAnalyzer
ARC0024Generated value names must be non-empty and uniqueErrorGeneratedValueAnalyzer
ARC0025Provide supplies a generated value typeWarningGeneratedValueAnalyzer
ARC0026Invalid explicit query declarationErrorReadModelAnalyzer and query metadata generation
ARC0027Generated value implements both generation contractsWarningGeneratedValueAnalyzer
ARC0028Move inline generation to a generated parameterInfoInlineGeneratedValueAnalyzer

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. Methods with a parameter of their own read-model type, nullable or not, are excluded; generic self-parameter helpers are not query candidates. Query invocation cannot close a genuine query’s type parameters. Make the query non-generic, move a generic composition helper off the read model, or opt into explicit query selection and leave the helper unmarked. 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]. Self-parameter methods are never queries and are ignored. In explicit mode only selected queries are analyzed; unmarked helpers are ignored. 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.

Reports [AllowAnonymous] declared on the same class or method as [Authorize] or [Roles]. One declaration that both opens itself to anonymous callers and restricts itself to authenticated or role-holding ones has no defensible reading. Arc refuses to guess: at run time it rejects the call with AmbiguousAuthorizationLevel, whichever attribute families are involved. The analyzer reports the contradiction at build time instead of on the first request. Keep one of the two.

[Command]
[Authorize]
[AllowAnonymous] // ARC0019
public record OpenAccount(string Name)
{
public void Handle() { }
}

Overriding a class across two declarations is a different, supported thing and is not reported: an [Authorize] read model can mark one query method [AllowAnonymous], which is how a protected read model exposes a login-screen query. Both Arc’s and ASP.NET Core’s attributes are recognized, but a declaration using only ASP.NET Core’s attributes on a type that is not an Arc artifact, such as an MVC controller, is left to ASP.NET Core.

ARC0020: ASP.NET Core authorization attributes

Section titled “ARC0020: ASP.NET Core authorization attributes”

Reports [Authorize] or [AllowAnonymous] from Microsoft.AspNetCore.Authorization on a [Command] or [ReadModel] type or method, in a project that does not reference Arc’s ASP.NET Core integration (Cratis.Arc). That integration is what enforces the ASP.NET Core attributes on model-bound artifacts. Without it - on Arc Core’s own HTTP host, for example - nothing reads them, and a command marked with one stays open to every caller.

using Microsoft.AspNetCore.Authorization;
[Command]
[Authorize] // ARC0020 in a project without Cratis.Arc: this command is not protected
public record CloseAccount(string Id)
{
public void Handle() { }
}

Use the Arc attribute of the same name, Cratis.Arc.Authorization.AuthorizeAttribute or AllowAnonymousAttribute, which every Arc host enforces. When a file imports both namespaces, qualify the attribute or alias it. Controllers are not reported; ASP.NET Core MVC enforces its own attributes there.

Arc v18.2.0 through v22.20.0 did not enforce the ASP.NET Core attributes on model-bound artifacts even with Cratis.Arc, because its evaluators resolved Arc’s same-named attributes instead (#2719).

ARC0021: Unevaluated authorization settings

Section titled “ARC0021: Unevaluated authorization settings”

Reports AuthenticationSchemes on an authorization attribute of a [Command] or [ReadModel] type or method when the project does not reference Arc’s ASP.NET Core integration. Native named policies are evaluated asynchronously on either Arc host. Explicit schemes require ASP.NET Core’s authentication service; the standalone Core host fails startup rather than silently admitting a caller.

[Command]
[Authorize(AuthenticationSchemes = "Bearer")] // ARC0021 without Cratis.Arc
public record UpdateProfile(string Name)
{
public void Handle() { }
}

Use ASP.NET Core integration and register the scheme, or omit AuthenticationSchemes and use a host-neutral named policy. Missing or ambiguous policy registrations also fail startup. Controllers are not reported; ASP.NET Core MVC evaluates their policies and schemes independently.

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
ARC0028Move to a [GeneratedValue] parameterMoveToGeneratedValueCodeFixProvider

ARC0010 and ARC0011 expose Roslyn’s batch Fix All support; ARC0028 deliberately fixes one expression at a time. 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.