You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while landing the four inline-code contexts (#61). The contexts now exist and are documented, but their payload members are dynamic — CommandContext.Command, QueryContext.Arguments, RuleContext.Artifact, RuleContext.Value, PolicyContext.Artifact — and dynamic is actively hostile to the code that has to live inside them.
Extension methods do not bind on dynamic, so LINQ fails at runtime
C# resolves extension methods statically; a dynamic receiver skips that lookup entirely. Verified with a minimal repro against .NET 10:
member access ok, count=2
LINQ on dynamic FAILED: RuntimeBinderException
'System.Collections.Generic.List<Line>' does not contain a definition for 'Sum'
after static assignment, LINQ ok, total=42
So context.Artifact.Lines.Sum(l => l.Amount) compiles and then throws at runtime. The workaround is to assign to a static type first:
List<Line>lines=context.Artifact.Lines;// now LINQ bindsvartotal=lines.Sum(l =>l.Amount);
That works, but nothing about the API suggests it, and the failure arrives at runtime in a validation rule rather than at author time.
Why this matters more than it looks — measured
Measuring a large production Cratis application (Ada) against the current language:
92 whole-command validation rules will land as inline code no matter what the language does. 43 of those 92 call a method or use LINQ — precisely the shape that fails.
65 Must and 22 MustAsync predicates on top of that.
402.Must( / .MustAsync( call sites in total.
The inline escape hatch is not a rare corner; it is where a real application's hardest domain logic lives. Making it awkward is a tax on the most important code in the document.
It also blocks two other issues
No autocompletion for inline languages, scoped to the sandbox they run in #65 (autocompletion for inline languages, scoped to the sandbox) — you cannot offer meaningful completion against dynamic. There is no static shape to complete on, so the sandbox scoping this issue asks for cannot be delivered while the payload is dynamic. This is a hard blocker, not a quality concern.
Query arguments are untyped and unchecked in the query context #78 (query arguments are untyped and unchecked) is the query-specific instance of exactly this problem. Whatever shape solves it should solve the other four members too, rather than being fixed one member at a time.
A secondary cost: dynamic is AOT- and trim-hostile
The same repro raises IL2026 and IL3050:
Using dynamic types might cause types or members to be removed by trimmer.
The 'dynamic' feature requires runtime-code generation, which is incompatible with AOT.
Anything rendered from a document whose inline code touches these contexts inherits that constraint.
Suggested direction
Syntax open, and this is deliberately not a proposal to remove the escape hatch's flexibility. Options worth weighing:
Generate a typed context per artifact. The document already declares the command's inputs, the rule's target and the read models in scope, so the shape is known at render time — the renderer could emit a context typed to that slice. This is the option that also unblocks No autocompletion for inline languages, scoped to the sandbox they run in #65.
Expose a typed accessor alongside the dynamic one — e.g. context.As<T>() — keeping the loose form for one-liners while giving real rules a static type.
This bears directly on the design-constraint ruling. The case for keeping Screenplay statement-shaped rather than growing an expression language rests on the inline block being a good place for real logic to live — that is the pressure valve the whole argument depends on. Today the valve makes LINQ throw at runtime and cannot be autocompleted. Whichever way #81 goes, this wants fixing; if it goes the statements-only way, it becomes load-bearing.
Found while landing the four inline-code contexts (#61). The contexts now exist and are documented, but their payload members are
dynamic—CommandContext.Command,QueryContext.Arguments,RuleContext.Artifact,RuleContext.Value,PolicyContext.Artifact— anddynamicis actively hostile to the code that has to live inside them.Extension methods do not bind on
dynamic, so LINQ fails at runtimeC# resolves extension methods statically; a
dynamicreceiver skips that lookup entirely. Verified with a minimal repro against .NET 10:So
context.Artifact.Lines.Sum(l => l.Amount)compiles and then throws at runtime. The workaround is to assign to a static type first:That works, but nothing about the API suggests it, and the failure arrives at runtime in a validation rule rather than at author time.
Why this matters more than it looks — measured
Measuring a large production Cratis application (Ada) against the current language:
Mustand 22MustAsyncpredicates on top of that..Must(/.MustAsync(call sites in total.The inline escape hatch is not a rare corner; it is where a real application's hardest domain logic lives. Making it awkward is a tax on the most important code in the document.
It also blocks two other issues
dynamic. There is no static shape to complete on, so the sandbox scoping this issue asks for cannot be delivered while the payload isdynamic. This is a hard blocker, not a quality concern.A secondary cost:
dynamicis AOT- and trim-hostileThe same repro raises
IL2026andIL3050:Anything rendered from a document whose inline code touches these contexts inherits that constraint.
Suggested direction
Syntax open, and this is deliberately not a proposal to remove the escape hatch's flexibility. Options worth weighing:
context.As<T>()— keeping the loose form for one-liners while giving real rules a static type.dynamicand document the assignment workaround. Cheapest, and what Define the rule and policy contexts for inline code #61 shipped for now, but it leaves No autocompletion for inline languages, scoped to the sandbox they run in #65 blocked and the failure mode at runtime.Relationship to #81
This bears directly on the design-constraint ruling. The case for keeping Screenplay statement-shaped rather than growing an expression language rests on the inline block being a good place for real logic to live — that is the pressure valve the whole argument depends on. Today the valve makes LINQ throw at runtime and cannot be autocompleted. Whichever way #81 goes, this wants fixing; if it goes the statements-only way, it becomes load-bearing.