Skip to content

Log vs Log T

thushxr edited this page Jun 27, 2026 · 1 revision

Log vs Log<T>

This library gives you two loggers: the non-generic Log and the generic Log<T>. They share a name on purpose — exactly like ILogger and ILogger<T> — but they are not interchangeable. This page explains the difference, why it exists, and how to choose.

The short answer

You want… Use Message style
Structured {placeholder} fields you can filter on Log Templates
The class + method an entry came from Log<T> Plain strings
// Log — message templates; named values become JSON fields:
Log.Information("User {userId} logged in from {ip}", "asda", "10.0.0.1");
// {"…","message":"User asda logged in from 10.0.0.1","userId":"asda","ip":"10.0.0.1"}

// Log<T> — class & method context, captured for free:
Log<OrderService>.Information("Processing started");
// {"…","class":"OrderService","method":"Process","message":"Processing started"}

⚠️ The gotcha

A {placeholder} passed to Log<T> is logged literallyLog<T> does not perform template substitution:

Log<OrderService>.Information("User {userId} logged in", 42);
// message: "User {userId} logged in"   ← NOT substituted
// and `42` is treated as the caller-info argument, not a template value

If you need template fields, use Log. If you need class/method context, use Log<T>. You cannot get both from a single call — see Why? below.

What each one captures

Field Log Log<T>
message (with {placeholder} substitution) ❌ (plain string)
{placeholder} → JSON fields
class typeof(T).Name
method [CallerMemberName]
timestamp, level, icon, traceId, exception

Both loggers share the same backend, the same output formatting, the same trace scope, and the same configuration. The only difference is whether class/method are supplied.

Why are they separate? (the technical reason)

There are two ways to capture a method name in .NET:

  1. [CallerMemberName] — the compiler bakes the caller's method name in as a default argument value. Zero runtime cost, no stack walk, no PDBs. This is what Log<T> uses.
  2. Stack walk — inspect the call stack at runtime. Costly, and degrades as the stack deepens. This library deliberately avoids it.

To use [CallerMemberName], the method needs a trailing optional parameter:

public static void Information(string message, [CallerMemberName] string? method = null)

But the non-generic Log already spends its trailing slot on the message-template values:

public static void Information(string message, params object?[] args)

A params array and a defaulted [CallerMemberName] parameter can't coexist — C# won't let an optional parameter follow params, and even if it could, the params array would swallow the caller-info argument. So the design splits cleanly:

  • Log keeps params object?[] args → message templates, no caller info.
  • Log<T> takes the caller-info slot → class/method, plain-string messages.

Log<T> doesn't need [CallerMemberName] for the class (it has typeof(T).Name for free) — it only uses it for the method.

Why share the name Log?

Log (arity 0) and Log<T> (arity 1) are genuinely distinct CLR types that happen to share a short name. This mirrors the convention every .NET developer already knows — ILogger / ILogger<T> — so Log and Log<T> read as "one logging concept, optionally typed," and IntelliSense surfaces both when you type Log.

The trade-off (and the reason for the gotcha above) is that, unlike ILogger<T> — which is just ILogger plus a category and has identical methods — our Log<T> has a different contract from Log. Same name, different capabilities. Keep the table at the top in mind and you won't be caught out.

Turning class/method off

UseClassName(false) and UseMethodName(false) suppress those fields:

Log.Configure(o => o.UseMethodName(false));

Log<OrderService>.Information("started");
// [OrderService] started      ← class kept, method suppressed

These two toggles only affect Log<T>. The non-generic Log never captures a class or method, so they are no-ops for it.

Rule of thumb

  • Logging a business/structured event with values to filter on → Log.
  • Logging from inside a service/handler class where the source location matters → Log<T>.

Clone this wiki locally