Skip to content

Inline code contexts are dynamic, so LINQ throws at runtime and autocompletion cannot work #84

Description

@woksin

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 binds
var total = 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

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:

  1. 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.
  2. 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.
  3. Keep dynamic and 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions