-
Notifications
You must be signed in to change notification settings - Fork 23
fix: support ICustomFormatter
via user-supplied IFormatProvider
#187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
string.Format
ICustomFormatter
via user-supplied IFormatProvider
Hi! Thanks for digging into this.
Would also be worth including a test to show where the behavior will differ with a custom formatter; there's a similar test in https://github.com/serilog/serilog/blob/dev/test/Serilog.Tests/Formatting/Display/MessageTemplateTextFormatterTests.cs#L168 (though it's a bit lean/minimal :-)). |
Hi @nblumhardt! Thanks for checking this out!
You're right, only if the parameter is of type
I think that's solved by virtue of the fact that we are still special-casing non-scalars and JSON-serializing them: ![]()
Good point; I'll add a test for this. |
I just added a couple of test cases (for testing I also special-cased Either way, I didn't touch the old tests, and all the tests (+ the new ones) are now passing, including the JSON test. |
Thanks for following up! Sorry about the slow reply. Formatting through |
Hi @nblumhardt — no worries! |
|
||
if (value is IFormattable formattable) | ||
{ | ||
output.Write(formattable.ToString(format, formatProvider)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deleted code here is all still required, without it, the format provider isn't passed through IFormattable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. Custom formatters often do this internally if they see fit.
Here's an example
public sealed class LogMessageFormatter(IFormatProvider? fallbackFormatter = null) : IFormatProvider, ICustomFormatter
{
public static LogMessageFormatter WithInvariant { get; } = new(CultureInfo.InvariantCulture);
public string Format(string? format, object? arg, IFormatProvider? _)
{
return arg switch
{
// NOTE: Print timestamps based on the host machine's timezone.
Instant instant => instant
.InZone(DateTimeZoneProviders.Tzdb.GetSystemDefault())
.ToString("yyyy-MM-ddTHH:mm:sso<g>", formatProvider: fallbackFormatter), // NOTE: NodaTime's ZonedDateTime's format strings: https://nodatime.org/3.2.x/userguide/zoneddatetime-patterns
// NOTE: Truncate GUIDs (because they're long AF), and only show the last 12 characters.
IId id => id.Value.ToString()[^12..], // NOTE: We choose the last part (rather than the first bit, which is often more common when it comes to UUIDs), because we're using UUIDv7 which have their random bits at the end, the first ones signify the timestamp and are therefore prone to collision.
// NOTE: Display the type's name rather than its fully-qualified name.
Type type => type.SimpleName(),
// NOTE: For anything else, if the object implements `IFormattable` (which means that it has a `.ToString()` overload that receives a format string), we want to use that. This is important in cases where the argument's format string is provided in the template (such as console template's `Timestamp`).
IFormattable thing => thing.ToString(format, fallbackFormatter),
_ => arg?.ToString() ?? "NULL",
};
}
public object? GetFormat(Type? formatType)
{
return formatType == typeof(ICustomFormatter)
? this
: fallbackFormatter?.GetFormat(formatType);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's no custom formatter, though, we just fall through to output.Write(value)
(no format provider used).
Fixes #186
I don't believe this is a breaking change. Because the hand-written
if
clauses and whatnot that were previously there were doing, in my analysis, effectively a strict subset of whatScalarValue.Render
does internally (and the latter does more, which is the crucial fix here, see #186).P.S. There are two other approaches that you can see in the first and second commits. But I think delegating to
ScalarValue.Render
is the most correct one.