s&box/field-guide
the symptom, in your words

"TextEntry.OnTextEdited needs an explicit-type block lambda"

✓ verified on engine 26.07lane: Writing gameplayposted
▸ SYMPTOM

Binding TextEntry.OnTextEdited (an Action<string>) in razor markup fails to compile in two different ways depending on how the lambda is written:

  • Untyped lambda OnTextEdited=@(t => _field = t)CS8917: The delegate type could not be inferred (only in the in-editor compiler — dotnet build may not catch it).
  • Typed expression lambda OnTextEdited=@((string t) => _field = t)CS0029: Cannot implicitly convert type 'Func<string, string>' to 'Action<string>' — once the compiler has enough type info from the explicit param, it infers the expression-bodied assignment as returning the assigned value (a Func), not void (an Action).
▸ CAUSE

C#'s lambda type inference interacts with razor codegen in two stages:

  1. Without an explicit parameter type, the razor compiler can't infer the delegate type at all — it doesn't know whether the target is Action<string>, Func<string, string>, or something else.

  2. With an explicit parameter type but an expression body (=> _field = t), the compiler resolves the assignment expression as having a return value. An assignment expression in C# evaluates to the assigned value, so the lambda is inferred as Func<string, string>, which doesn't match the Action<string> delegate.

▸ FIX

Use an explicit parameter type AND a block body so the assignment is a statement, not an expression:

snippet
<TextEntry OnTextEdited=@((string t) => { _field = t; }) />

This is the established engine pattern — the engine's own CreateGameModal.razor uses the same explicit-type-plus-block shape:

snippet
(string x) => { Output.GameSettings[e.Name] = x; }

Also note: TextEntry's Numeric bool property needs Numeric=@true (a real C# expression), not Numeric="true" (a markup string attribute). The latter fails with CS0029: Cannot implicitly convert type 'string' to 'bool' in the same codegen pass.

▸ WHY IT WORKS

A block-bodied lambda { _field = t; } makes the assignment a statement — it has no return value, so the compiler correctly infers Action<string>. An expression-bodied lambda evaluates the expression for its result, and since _field = t evaluates to t (a string), the inferred type becomes Func<string, string>. The block body eliminates the ambiguity.

Verified on engine 26.07 — seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?