ReasonMetadata — Structured Diagnostic Metadata (v1.40.0)
Every Reason-derived type now carries a Metadata property of type ReasonMetadata — a lightweight sealed record capturing who created this error. It is completely separate from Tags (no framework leakage into your business metadata).
// ReasonMetadata is captured automatically — no extra code needed
public sealed record ReasonMetadata
{
public string? CallerMember { get; init; } // name of the method that created the error
public string? CallerFile { get; init; } // source file path
public int? CallerLine { get; init; } // line number
public static readonly ReasonMetadata Empty = new();
}
// Accessing metadata
var error = ValidationError.Field("Email", "Invalid format");
Console.WriteLine(error.Metadata.CallerMember); // name of the calling method — e.g. "ValidateUser"
// Reading from an IReason-typed reference via IReasonMetadata capability interface
IReason reason = error;
ReasonMetadata? meta = reason.TryGetMetadata(); // null if IReason doesn't implement IReasonMetadata
bool hasCaller = reason.HasCallerInfo(); // true when CallerMember is set
Three data planes — clean separation:
| Plane | Storage | Purpose |
|---|---|---|
Message |
string |
Human-readable description for logs and UI |
Tags |
ImmutableDictionary<string, object> |
Business/domain context (HttpStatusCode, FieldName, …) |
Metadata |
ReasonMetadata |
System/diagnostic context (who created this error) |
JSON serialization — "metadata" is written only when Metadata != Empty; reading is backward-compatible (missing key → Empty):
{
"type": "ValidationError",
"message": "Invalid format",
"tags": { "ErrorType": "Validation", "FieldName": "Email" },
"metadata": { "CallerMember": "ValidateEmail", "CallerFile": "/src/UserService.cs", "CallerLine": 42 }
}