Skip to content

Rendering a $context path emits members Arc's CommandContext does not have #32

Description

@woksin

Rendering a $context path emits members Arc's CommandContext does not have, so the rendered application does not compile.

What happens

ExpressionRenderer renders every context expression by PascalCasing the declared path against a fixed parameter named context:

ContextExpressionSyntax context => RenderContextPath(context.Path),   //  "context." + PascalCase(path)
EventSourceIdExpressionSyntax => "context.EventSourceId",
CausedByExpressionSyntax causedBy => $"context.CausedBy.{}",

That is right where Chronicle's EventContext is in scope — a reactor method, a projection. It is wrong in a command handler, which is where most $context reads in a real document land. A rendered command handler is an Arc model-bound Handle(), and Arc's Cratis.Arc.Commands.CommandContext carries CorrelationId, Type, Command, Dependencies, Values, AllowedSeverity, Response, ServiceProvider, CancellationToken — and none of Occurred, Identity, Tenant, CausedBy, Causation.

Those five are declared on Screenplay's own Cratis.Screenplay.Contexts.CommandContext, a different type that a rendered application never receives. The two share a simple name, so the renderer's using Cratis.Arc.Commands; binds the wrong one and every read fails to compile.

Measured

Compiled Screenplay/Source/DotNET/Screenplay/for_ScreenplayCompiler/invoicing.play (17 slices) with ScreenplayCompiler, rendered it with CratisRenderer.CreateDefault()'s renderer set into InMemoryCodeOutput, and compiled the result in memory against the real Cratis assemblies plus the global usings the Cratis package contributes.

30 compilation errors. 15 of them are the renderer's own rendering:

Rendered Errors Files
context.Occurred 11 RegisterInvoice, CancelInvoice, ChangeInvoiceStatus, ApplyDiscount, WriteOffInvoice
context.Identity.Id 2 RegisterInvoice, CancelInvoice
context.Tenant 1 RegisterInvoice
context.CausedBy.Subject / context.Causation.Type 1 RegisterInvoice, CancelInvoice

The other 15 come from authored csharp blocks in the document and from InvoiceLineItemAdded, an event the document references but never declares — separate problems, listed below.

Why it is the "invented value" case

The document says registeredAt = $context.occurred. The renderer answers with a member access that does not exist, rather than with the value or with a report. Nothing in the output says the value was not delivered — the file simply does not build, and it builds less the more carefully the document states its provenance.

What a rendered handler can actually reach

Every path the language names has a real counterpart the Cratis runtime registers; a command handler's parameters resolve from DI by type, so asking for one is how a hand-written slice reaches the same value:

Path Rendering Collaborator
$context.occurred DateTimeOffset.UtcNow
$context.tenant tenants.Current.Value Cratis.Arc.Tenancy.ITenantIdAccessor
$context.identity.id / $context.causedBy.subject identities.GetCurrent().Subject Cratis.Chronicle.Identities.IIdentityProvider
$context.identity.name / .userName .Name / .UserName same
$context.identity.isAuthenticated principals.Current?.Identity?.IsAuthenticated == true Cratis.Arc.Authorization.ICurrentPrincipalAccessor
$context.identity.roles (principals.Current?.FindAll(ClaimTypes.Role) ?? []).Select(…) same
$context.identity.claims.<name> principals.Current?.FindFirst("<name>")?.Value ?? string.Empty same
$context.causation.type causations.GetCurrentChain()[^1].Type.Value Cratis.Chronicle.Auditing.ICausationManager
$context.command.<property> the command's own property

$eventContext.* and $eventSourceId have no counterpart in a command handler at all — it runs before anything is appended — so those are drops, not renderings.

IIdentityProvider has to be named in full. The Cratis package's GlobalUsings.cs brings in both Cratis.Arc.Identity and Cratis.Chronicle.Identities, and each declares an IIdentityProvider, so the short name is ambiguous in every rendered file whether or not the renderer adds a using of its own. This is only visible in a compilation that includes the package's global usings — the spec harness did not, which is why no spec saw it.

A context value can also be unfillable. invoicing.play maps registeredFor = $context.tenant onto an event property declared as TenantId, a Uuid concept. The runtime's tenant is a string, and no conversion exists in either direction. That is a genuine drop: it has to be reported, not rendered into a cast that cannot compile.

Not this issue, found by the same run

Listing them so the remaining 15 errors on invoicing.play are not read as this one:

  • Authored csharp blocks (11 errors). ProcessInvoiceBatch and the DetectOverdueInvoices reactor are handled by inline code written against Screenplay's contexts. A rendered application has no Cratis.Screenplay reference and receives Arc's CommandContext, so context.Identity.Id in an authored block cannot compile either. The block also references types from sibling slices (InvoiceSent, InvoiceCancelled, MarkInvoiceOverdue) that the renderer does not emit usings for, because it does not analyze raw code text. Whether a rendered application should reference Cratis.Screenplay so authored blocks compile against the context they were written for is a decision, not a defect.
  • InvoiceLineItemAdded (3 errors). The document projects from an event it never declares. Already reported by the renderer; the emitted [FromEvent<InvoiceLineItemAdded>] still names it.
  • The DetectOverdueInvoices block itself reads DueDate and InvoiceId unqualified and has a code path that returns nothing — the document's own C#, emitted verbatim.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions