Lambda Expressions
A lambda call invokes a registered Func<…> delegate from any tag. Use lambdas for formatting, encoding, sorting, arithmetic, and any other logic the tag syntax does not handle directly.
Call syntax
{ (arg1, arg2, …) => funcName }An argument list in parentheses, the lambda operator =>, then the registered function name. Whitespace inside the tag is insignificant; the parentheses delimit the argument list.
A zero-argument call uses empty parentheses:
{ () => now }Where lambda calls are valid
Lambda calls can appear inside any of the following tags:
- Simple tags —
{(FirstName, LastName) => formatName} - Conditional tags —
{#if (User) => isAdmin} - Iteration tags —
{#each (Items) => sort} - With tags —
{#with (Customer) => primaryAddress} - Variable declaration / reassignment —
{?var:n = (Name) => upper} - Partial tags —
{>(Kind) => selectTemplate} - Debug tags —
{@ (Items) => describe}
Anywhere the engine expects an expression, a lambda call is acceptable.
Registering a function
Use LambdaRepo.Register(name, delegate). The function can be any Func<…> (or any other delegate type with a matching signature).
var person = new
{
Certifications = new[] { "mcse", "mcitp", "mcts" },
Name = new { First = "John", Last = "Doe" }
};
string template = "Hello {Name.First} {Name.Last} — certs: {(', ', Certifications) => join}.";
Func<string, object[], string> join = (delim, values) => string.Join(delim, values);
var ngin = new TemplateEngine(template);
ngin.LambdaRepo.Register(nameof(join), join);
string result = ngin.Merge(person);
// Hello John Doe — certs: mcse, mcitp, mcts.Result:
Hello John Doe — certs: mcse, mcitp, mcts.The repository is keyed by name (case-sensitive, ordinal). Registering the same name twice throws. Use LambdaRepo.Deregister(name) to remove a registration.
LambdaRepo is lazily created — accessing it the first time allocates an empty repository. Register functions before calling Merge; the engine resolves lambda calls during merge by name lookup.
Argument forms
Each argument in the call list can be any of:
| Form | Example |
|---|---|
| Bound expression | {(Address.City) => upper} |
$ local-scope reference | {($) => stringify} |
| Scope-walk expression | {(..\Title) => upper} |
| Declared variable | {(:count) => format} |
| String literal | {("hello") => upper} or {('hello') => upper} |
| Numeric literal | {(42) => format} or {(3.14) => format} |
| Boolean literal | {(true) => negate} |
| Char literal | {(',') => splitOn} — quoted literal of length 1 |
| DateTime literal | {("2025-01-01") => formatDate} — any quoted string parseable by DateTime.TryParse |
Literal coercion is driven by the target parameter’s type. A numeric literal is parsed against the parameter’s exact type (int, decimal, double, short, byte, etc.) when the function is invoked — no f/d/m suffixes are needed. A quoted literal becomes a string, char, or DateTime depending on the parameter type. Bound expression arguments must already match the parameter type or be convertible to it.
A lambda call result cannot appear directly as an argument to another lambda call. Assign the first result to a variable and pass the variable:
{?var:result = (Name) => firstFunc}
{(:result, OtherArg) => secondFunc}String literal quoting
String literal arguments accept either single or double quotes:
| Literal | Tag form |
|---|---|
| Plain string | {("hello") => f} or {('hello') => f} |
| Contains a double quote | Use single quotes: {('She said "hi"') => f} |
| Contains a single quote | Use double quotes: {("It's easy") => f} |
| Contains both | Escape with backslash: {("It's \"easy\"") => f} |
Choose the outer quote style that avoids escaping; reach for backslash only when both quote types appear in the same literal.
Passing null explicitly
A bare null literal cannot appear in an argument list. To pass null to a function, declare an unassigned variable in scope and pass the variable:
{?var:nothing}
{(:nothing) => MyFunc}A declared variable with no initial value is null. The same :nothing variable can be reused for additional null arguments. See Variables.
Return values
The function’s return value becomes the tag’s result:
- In a simple tag, it is rendered into the output (converted via
ToString()). - In an
{#if}, the return value is evaluated against the falsy rules. - In an
{#each}, the return value must beIEnumerable; it is iterated. - In a
{#with}, the return value becomes the new local scope. - In a
{?var:}or{?:}, the return value is stored in the variable. - In a
{>partial}, the return value must be a non-null string template, which is then parsed and merged. Returningnull(or a non-string) throwsInvalidOperationException, surfaced as aMergeException. - In a
{@}, the return value is written to trace listeners (see Debugging).
Returning null is permitted in most contexts: a null simple-tag value renders as empty string; a null if-condition is falsy; a null each-target skips the block; a null variable assignment stores null. The one exception is {>partial}, which requires a string.
Exception behavior
Exceptions thrown inside a lambda propagate as the InnerException of a MergeException, with the surrounding template position attached via MergeException.Context. See Exception Handling.
Composition with variables
Variables and lambdas compose naturally for counters, accumulators, and conditional formatting. The running-total example in Variables shows the pattern: declare a variable, reassign it on each iteration via a lambda that does the arithmetic.
Next steps
- Variables — capture lambda return values for reuse later in the template
- Debugging —
{@ (…) => fn}runs a lambda for its side-effecting trace output - Exception Handling — pinpoint the template position when a lambda throws