Commands
Commands are input definitions — imperative intents. A command declares its properties, its authorization, its validation rules, and what events it produces.
Syntax
Section titled “Syntax”command <Name> <property> <Type>[?] ...
[authorize <PolicyName> [<PolicyName>]*]
[validate <rule> message "<message>" ...]
[validate csharp ``` <C# yielding ValidationError> ```]
[produces ...] ← declarative — repeatable
[handler ← imperative fallback — instead of produces file <Path> | csharp ``` <C# returning the events to append> ```]Validation rules
Section titled “Validation rules”Declarative validation covers the common cases without code:
| Rule | Example |
|---|---|
not empty | name not empty |
max <n> | reason max 500 |
min <n> | quantity min 1 |
> <value> | quantity > 0 |
>= <value> | discountPct >= 0 |
< <value> | dateOfBirth < today |
length == <n> | currency length == 3 |
matches <regex> | email matches email |
matches "<pattern>" | invoiceNumber matches "^INV-[0-9]{6}$" |
all > <value> (on collection) | lines.quantity all > 0 |
Every rule carries a message shown when it fails:
validate invoiceNumber not empty message "Invoice number is required" invoiceNumber matches "^INV-[0-9]{6}$" message "Invoice number must match INV-000000" dueDate > today message "Due date must be in the future"Cross-field or complex rules drop into C#:
validate csharp ``` var total = Lines.Sum(l => l.Quantity * l.UnitPrice); if (total > 1_000_000 && PaymentTerms == PaymentTerms.Immediate) yield ValidationError("Invoices over 1,000,000 cannot require immediate payment"); ```Both validate and validate csharp can coexist on the same command.
Authorization
Section titled “Authorization”authorize <PolicyName> [<PolicyName>]*Multiple policies must all pass. Continuation lines list additional policies; or makes policies alternatives:
authorize CanManageInvoice IsAdultCustomer
authorize IsAccountant or IsCustomerSelfPolicies are declared at the top of the file — see Policies.
The produces block
Section titled “The produces block”Declares what events a command emits. Supports single, multiple, and conditional forms. For a fully imperative implementation, use a handler instead.
Single event with property mapping
Section titled “Single event with property mapping”produces InvoiceRegistered invoiceId = invoiceId // from command property registeredAt = $context.occurred // from event context registeredBy = $context.identity.id // caller identity source = $env.SERVICE_NAME // environment variable status = "draft" // string constant lineCount = 0 // numeric constantMapping sources
Section titled “Mapping sources”| Source | Syntax | Description |
|---|---|---|
| Command property | = <propertyName> | Direct copy from command |
| Event context | = $context.occurred | Timestamp of the event |
| Caller identity | = $context.identity.id | Subject from auth token |
| Environment | = $env.<VAR_NAME> | Environment variable |
| String constant | = "value" | Literal string |
| Numeric constant | = 0 | Literal number |
| Expression | = lines.sum(l => l.quantity * l.unitPrice) | Computed value |
Multiple unconditional events
Section titled “Multiple unconditional events”Repeat produces for each event; all are emitted:
produces InvoiceLineItemAdded invoiceId = invoiceId addedAt = $context.occurred
produces InvoiceRunningTotalUpdated invoiceId = invoiceId adjustment = lines.sum(l => l.quantity * l.unitPrice * (1 - l.discountPct / 100))Conditional produces
Section titled “Conditional produces”produces when <condition> emits the indented event only when the condition holds. Conditions compare command properties, constants, and environment variables with ==, !=, >, >=, <, <=, combined with and/or:
produces when isProForma == true ProFormaInvoiceIssued invoiceId = invoiceId issuedAt = $context.occurred
produces when paymentTerms == "net30" or paymentTerms == "net60" DeferredPaymentInvoiceRegistered invoiceId = invoiceId paymentTerms = paymentTerms
produces when $env.WELCOME_EMAILS_ENABLED == "true" CustomerWelcomeEmailRequested customerId = customerIdMultiple produces when blocks form mutually exclusive or overlapping branches — each condition is evaluated independently.
The handler block
Section titled “The handler block”Declares a fully imperative implementation of the command, in C#, as either an inline block or a reference to an external file. Use it when the declarative produces forms cannot express the logic — batch processing, imperative branching, or anything that needs more than property mappings and conditions.
Delegating to a file:
handler file Commands/ProcessInvoiceBatchHandler.csInline C#:
handler csharp ``` var events = new List<object>(); events.Add(new InvoiceBatchProcessingStarted( BatchId: BatchId, StartedAt: DateTimeOffset.UtcNow )); foreach (var invoiceId in InvoiceIds) events.Add(new InvoiceSent(invoiceId, DateTimeOffset.UtcNow, context.Identity.Id, null)); return events; ```A command uses either produces blocks or a handler — not both. Keep handler logic small; anything substantial belongs in a file reference where it can be tested on its own.