Serilog - PawelGerr/Thinktecture.Runtime.Extensions GitHub Wiki
Structured logging with Serilog is improved by teaching Serilog how to destructure Thinktecture types. Without the integration, a keyed Smart Enum like OrderStatus.Paid logs as a large object graph instead of its underlying key. With it, all Thinktecture types render in their natural, compact form.
- Installation
- Setup
- Behavior by Type Family
- Rendering as Strings
- Types Declined by the Destructurer
- Caveats
dotnet add package Thinktecture.Runtime.Extensions.Serilog
Requires Serilog >= 4.0.0.
Add using Thinktecture; and call UsingThinktectureRuntimeExtensions() when configuring the destructuring policy:
using Thinktecture;
Log.Logger = new LoggerConfiguration()
.Destructure.UsingThinktectureRuntimeExtensions()
.WriteTo.Console()
.CreateLogger();Call the method once, before CreateLogger(). No other configuration is required.
A keyed Smart Enum ([SmartEnum<TKey>]) unwraps to its underlying key. A simple Value Object ([ValueObject<TKey>]) unwraps to its key the same way. Object factories are deliberately ignored for logging: the key is always used, even when an [ObjectFactory<T>] attribute is present.
[SmartEnum<string>]
public partial class OrderStatus
{
public static readonly OrderStatus Pending = new("Pending");
public static readonly OrderStatus Paid = new("Paid");
public static readonly OrderStatus Shipped = new("Shipped");
}
[ValueObject<decimal>]
public partial struct Amount;
// Usage
var status = OrderStatus.Paid;
var amount = Amount.Create(99.95m);
Log.Information("Order status: {@Status}, amount: {@Amount}", status, amount);
// Logs: Order status: "Paid", amount: 99.95An ad-hoc union ([Union<T1, T2, ...>]) unwraps to its current Value. When the active member is itself a Thinktecture type (a Smart Enum, Value Object, or another union), the destructurer recurses into it:
[Union<OrderStatus, string>]
public partial struct OrderOrError;
// Usage
OrderOrError result = OrderStatus.Paid;
Log.Information("Result: {@Result}", result);
// Logs: Result: "Paid" (unwrapped through the union and the Smart Enum)The destructurer composes recursively (destructureObjects: true). A complex POCO that contains Thinktecture properties is handled by Serilog's default object destructuring, with each Thinktecture property unwrapped in turn:
public record Order(OrderStatus Status, Amount Total);
var order = new Order(OrderStatus.Paid, Amount.Create(149.99m));
Log.Information("Order: {@Order}", order);
// Logs: Order: {Status: "Paid", Total: 149.99}Because the destructurer recurses with destructureObjects: true, the inner value of a Thinktecture type is handed back to Serilog's full destructuring pipeline. A Thinktecture inner value is unwrapped by this integration, but a non-Thinktecture inner value -- for example an ad-hoc union whose current value is a plain POCO -- is destructured by Serilog's default reflection. For a large or deeply nested object graph that can produce verbose log events.
This is the same recursion Serilog applies to any {@Property}, so it is bounded with Serilog's built-in limits rather than a Thinktecture-specific setting:
Log.Logger = new LoggerConfiguration()
.Destructure.UsingThinktectureRuntimeExtensions()
.Destructure.ToMaximumDepth(3) // cap nesting depth
.Destructure.ToMaximumCollectionCount(10) // cap collection elements
.Destructure.ToMaximumStringLength(1000) // cap string length
.WriteTo.Console()
.CreateLogger();These limits apply to all destructuring, so they also cap any object graph reached through a Thinktecture type.
By default, Thinktecture types are logged as their underlying scalar (the key, or the union's current value). You can opt into logging them as strings instead by passing a TypesToRenderAsString flag.
using Thinktecture;
Log.Logger = new LoggerConfiguration()
.Destructure.UsingThinktectureRuntimeExtensions(
renderAsString: TypesToRenderAsString.SmartEnums | TypesToRenderAsString.ValueObjects)
.WriteTo.Console()
.CreateLogger();Available flag values:
| Value | Effect |
|---|---|
None |
Default: scalar, no string coercion |
SmartEnums |
Keyed Smart Enums log as string via ToString()
|
ValueObjects |
Simple Value Objects log as string via ToString()
|
AdHocUnions |
Ad-hoc Unions log as string via ToString()
|
All |
All three families log as string |
Values are [Flags], so combine them freely with |.
Without renderAsString:
Log.Information("Amount: {@Amount}", Amount.Create(99.95m));
// Logs: Amount: 99.95 (decimal scalar)With TypesToRenderAsString.ValueObjects:
Log.Information("Amount: {@Amount}", Amount.Create(99.95m));
// Logs: Amount: "99.95" (string)When a type is declared with SkipToString = true, the source generator does not emit a ToString() override. If you include that type family in renderAsString, the log entry will contain the type name (the CLR default) rather than the key value. There is no cheap runtime detection of this condition. Check whether your types use SkipToString = true before enabling string rendering.
The following types are not handled by the Thinktecture destructurer and fall through to Serilog's default behavior:
| Type | Reason |
|---|---|
| Keyless Smart Enums | No underlying key to unwrap |
| Complex Value Objects | Multiple properties; Serilog default object destructuring applies |
Regular Unions ([Union]) |
Inheritance-based; no single scalar value to extract |
| Plain POCOs | Not Thinktecture types |
These types are still logged -- Serilog's built-in destructuring applies to them as usual.
default(struct union) throws. A struct ad-hoc union that has never been initialized (i.e., default(TUnion)) has no active member. Reading its Value throws InvalidOperationException. Ensure struct union values are always initialized before logging.
Object factories are ignored for logging. Even when [ObjectFactory<T>] is applied to a type, the integration always uses the underlying key (for Smart Enums and Value Objects). The object factory's custom ToValue() is not called.
Serilog @ operator required. Use the {@Property} syntax (the destructuring operator) to trigger the Thinktecture policy. Without @, Serilog calls ToString() directly and bypasses the destructurer.