-
Notifications
You must be signed in to change notification settings - Fork 0
Message Templates and Structured Logging
The non-generic Log uses Serilog-style message templates. A template is a string with
named {placeholder} tokens; you pass the values positionally. Each value is rendered into the
message and (in JSON mode) emitted as its own field you can filter on.
Templates are a feature of the non-generic
Log.Log<T>messages are plain strings — see Log vs Log<T>.
Log.Information("User {userId} logged in from {ip}", "asda", "10.0.0.1");Text output:
2026-06-27 09:14:02.214 🟩 [INFO] User asda logged in from 10.0.0.1
JSON output (UseStructured()):
{"timestamp":"2026-06-27 09:14:02.214","level":"Information","icon":"🟩","message":"User asda logged in from 10.0.0.1","userId":"asda","ip":"10.0.0.1"}userId and ip are now first-class fields — directly queryable in CloudWatch Insights,
Seq, Elastic, Loki, or any JSON log tool, without parsing the message string.
Placeholders bind to arguments left to right, by position (the same as Serilog). The placeholder name is the field key; it does not have to match the variable name.
Log.Information("{a} then {b}", first, second);
// a = first, b = secondIf you pass fewer values than placeholders, the missing ones render as empty / null. If
you pass more values than placeholders, the extras are ignored.
Values are serialized by their runtime type, so numbers stay numbers, booleans stay booleans:
Log.Information("Processed {count} items in {ok}", 3, true);{"…","message":"Processed 3 items in True","count":3,"ok":true}Note count is 3 (a JSON number) and ok is true (a JSON boolean) — not strings.
Every level has an exception overload. The exception is appended after the message (in text) or
written as an exception field (in JSON). You can still use template values:
try
{
ProcessPayment();
}
catch (Exception ex)
{
Log.Error(ex, "Payment failed for {orderId}", orderId);
}Text:
2026-06-27 09:14:02.500 🟥 [ERROR] Payment failed for 1234
System.InvalidOperationException: Gateway timeout
at Payments.Process() in C:\app\Payments.cs:line 42
JSON:
{"…","message":"Payment failed for 1234","orderId":1234,"exception":"System.InvalidOperationException: Gateway timeout\n at ..."}The logger writes a fixed set of built-in fields:
timestamp · level · icon · traceId · class · method · message · exception
If one of your placeholder names collides with a built-in field, the placeholder is skipped as a JSON field so it can't overwrite the real one. Consider:
Log.Information("Severity {level} reported", "high");Without protection you'd get two "level" keys, and most log tools do last-wins — your
"high" would clobber the real log level. Instead, the built-in field wins:
{"level":"Information","message":"Severity high reported"}The value isn't lost — it's still rendered into message ("Severity high reported"); it just
doesn't get a duplicate, colliding field. Avoid naming placeholders after reserved fields if
you want them as queryable fields. This protection applies to JSON only; text output has no
key/value structure to collide.
See Performance & Internals for how this is implemented.
The JSON encoder uses relaxed escaping (<, >, &, and emoji are written literally rather
than as \uXXXX), which keeps console output readable. This is safe for stdout / log files; it
is not intended for embedding directly into HTML.
Thushar.Logging · a zero-dependency static logger for .NET
📦 NuGet · 💻 GitHub · 🐛 Issues · 📸 Instagram
dotnet add package Thushar.Logging
Getting around
Features
Using it
Under the hood