Background and motivation
IFormattable, ISpanFormattable, and IUtf8SpanFormattable model formatting as an intrinsic capability of the value itself.
That works well when the target type owns its textual representation:
value.ToString(format, provider);
value.TryFormat(destination, out charsWritten, format, provider);
value.TryFormat(utf8Destination, out bytesWritten, format, provider);
However, some formatting scenarios require an external formatting strategy instead of members on the target type.
This is similar to the relationship between IComparable<T> and IComparer<T>:
IComparable<T> // intrinsic comparison capability
IComparer<T> // external comparison strategy
The corresponding formatting relationship would be:
IFormattable // intrinsic formatting capability
IFormatter<T> // external formatting strategy
External formatter strategies are useful when the target type:
- is defined in a third-party library;
- cannot be modified by the application;
- does not implement
IFormattable, ISpanFormattable, or IUtf8SpanFormattable;
- exposes formatting through a different pattern;
- needs multiple independent textual representations;
- should not own application-specific formatting rules.
There is currently no simple strongly-typed BCL strategy interface for formatting values externally.
API Proposal
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System
{
/// <summary>Defines a mechanism for formatting a value as a string.</summary>
/// <typeparam name="T">The type of value to format.</typeparam>
public interface IFormatter<in T>
{
/// <summary>Formats the value using the specified format.</summary>
/// <param name="value">The value to format.</param>
/// <param name="format">The format to use, or <c>null</c> to use the default format.</param>
/// <param name="provider">An object that provides culture-specific formatting information.</param>
/// <returns>The result of formatting <paramref name="value" /> using <paramref name="format" />.</returns>
/// <exception cref="FormatException"><paramref name="format" /> is not supported.</exception>
string Format(T value, string? format, IFormatProvider? provider);
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System
{
/// <summary>Defines a mechanism for formatting a value into a span of characters.</summary>
/// <typeparam name="T">The type of value to format.</typeparam>
public interface ISpanFormatter<in T> : IFormatter<T>
{
/// <summary>Tries to format the value into the provided span of characters.</summary>
/// <param name="value">The value to format.</param>
/// <param name="destination">The span in which to write the formatted value.</param>
/// <param name="charsWritten">On return, contains the number of characters written to <paramref name="destination" />.</param>
/// <param name="format">The format to use, or an empty span to use the default format.</param>
/// <param name="provider">An object that provides culture-specific formatting information.</param>
/// <returns><c>true</c> if the value was successfully formatted; otherwise, <c>false</c>.</returns>
bool TryFormat(
T value,
Span<char> destination,
out int charsWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider);
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System
{
/// <summary>Defines a mechanism for formatting a value as UTF-8 into a span of bytes.</summary>
/// <typeparam name="T">The type of value to format.</typeparam>
public interface IUtf8SpanFormatter<in T>
{
/// <summary>Tries to format the value as UTF-8 into the provided span of bytes.</summary>
/// <param name="value">The value to format.</param>
/// <param name="utf8Destination">The span in which to write the formatted UTF-8 bytes.</param>
/// <param name="bytesWritten">On return, contains the number of bytes written to <paramref name="utf8Destination" />.</param>
/// <param name="format">The format to use, or an empty span to use the default format.</param>
/// <param name="provider">An object that provides culture-specific formatting information.</param>
/// <returns><c>true</c> if the value was successfully formatted; otherwise, <c>false</c>.</returns>
bool TryFormat(
T value,
Span<byte> utf8Destination,
out int bytesWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider);
}
}
API Usage
Formatting with an external strategy
using System;
public static class FormatterExtensions
{
public static string FormatWith<T>(
this IFormatter<T> formatter,
T value,
string? format = null,
IFormatProvider? provider = null)
{
ArgumentNullException.ThrowIfNull(formatter);
return formatter.Format(value, format, provider);
}
public static bool TryFormatWith<T>(
this ISpanFormatter<T> formatter,
T value,
Span<char> destination,
out int charsWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider = null)
{
ArgumentNullException.ThrowIfNull(formatter);
return formatter.TryFormat(value, destination, out charsWritten, format, provider);
}
}
Generic code that does not require the target type to implement IFormattable
using System;
public static class FormattingPipeline
{
public static string ConvertToString<T>(
T value,
IFormatter<T> formatter,
string? format = null,
IFormatProvider? provider = null)
{
ArgumentNullException.ThrowIfNull(formatter);
return formatter.Format(value, format, provider);
}
}
Example type
The target type does not implement IFormattable, ISpanFormattable, or IUtf8SpanFormattable.
public readonly record struct Money(decimal Amount, string Currency);
Example external formatter
using System;
using System.Globalization;
public sealed class MoneyFormatter : ISpanFormatter<Money>
{
public string Format(Money value, string? format, IFormatProvider? provider)
{
format ??= "C A";
return format switch
{
"C A" => string.Create(
provider,
$"{value.Currency} {value.Amount:0.00}"),
"A C" => string.Create(
provider,
$"{value.Amount:0.00} {value.Currency}"),
_ => throw new FormatException()
};
}
public bool TryFormat(
Money value,
Span<char> destination,
out int charsWritten,
ReadOnlySpan<char> format,
IFormatProvider? provider)
{
if (format.IsEmpty || format.SequenceEqual("C A"))
{
return destination.TryWrite(
provider,
$"{value.Currency} {value.Amount:0.00}",
out charsWritten);
}
if (format.SequenceEqual("A C"))
{
return destination.TryWrite(
provider,
$"{value.Amount:0.00} {value.Currency}",
out charsWritten);
}
charsWritten = 0;
return false;
}
}
Usage:
IFormatter<Money> formatter = new MoneyFormatter();
string text = formatter.Format(
new Money(123.45m, "USD"),
"C A",
CultureInfo.InvariantCulture);
Design notes
Relationship to IFormattable
IFormatter<T> does not replace IFormattable.
IFormattable models an intrinsic capability of the value. IFormatter<T> models an external strategy object. Both are useful in different scenarios, just as both IComparable<T> and IComparer<T> are useful.
Relationship to ICustomFormatter
ICustomFormatter is object-based and provider-oriented:
string Format(string? format, object? arg, IFormatProvider? formatProvider);
IFormatter<T> would be strongly typed:
string Format(T value, string? format, IFormatProvider? provider);
This avoids boxing for value types, provides a generic type-safe contract, and composes naturally with generic code.
Why is T contravariant?
IFormatter<in T> only consumes values of type T. This allows formatter strategies to be used contravariantly where appropriate.
Why does IUtf8SpanFormatter<T> not inherit ISpanFormatter<T>?
UTF-8 formatting can be useful independently from UTF-16 formatting. Libraries that operate directly on UTF-8 payloads may not want a UTF-16 formatting dependency in their generic constraints.
A formatter can implement both interfaces when appropriate:
public sealed class SomeFormatter<T> :
ISpanFormatter<T>,
IUtf8SpanFormatter<T>
{
}
Argument order
The proposed argument order is:
Format(value, format, provider)
TryFormat(value, destination, out written, format, provider)
The value comes first because this is an external strategy operating on a supplied value. The rest of the order follows existing formatting APIs, where format precedes provider, and span formatting uses destination, out written, format, provider.
Alternative Designs
Keep using IFormattable
One option is to require target types to implement IFormattable, ISpanFormattable, or IUtf8SpanFormattable.
This works when the target type can be modified and when formatting behavior naturally belongs to the value itself. It does not work well for third-party types, sealed types, legacy types, or multiple independent formatting strategies.
Use ICustomFormatter
ICustomFormatter already allows external formatting through IFormatProvider.
However, it is object-based rather than strongly typed. It also does not provide span-based or UTF-8 formatting contracts.
Use delegates
Another option is to use delegates such as:
or custom delegate types.
This is lightweight, but it does not provide a standard shape for format strings, culture providers, span formatting, UTF-8 formatting, or reusable formatter objects.
Use extension methods
A library can define extension methods for particular target types.
This can be convenient, but it does not provide a general generic strategy interface that can be passed around, registered, composed, or dependency-injected.
Combine parser and formatter strategies into one interface
Another option is to define a single interface that contains both parsing and formatting members.
This may be useful for round-trip conversion scenarios, but it would force types to provide both directions even when only formatting is needed. Keeping parser and formatter strategy proposals separate makes the contracts smaller and more composable.
Use a different name
The name IFormatter<T> may be confused with existing formatting concepts such as ICustomFormatter and System.Runtime.Serialization.IFormatter.
Alternative names could include:
IValueFormatter<T>
ITextFormatter<T>
IFormatStrategy<T>
The proposed name follows the IComparer<T> analogy, but the final name should be reviewed carefully.
Risks
Increased public API surface area
The proposal adds new public interfaces to System. This increases the long-term API surface area and should be justified by concrete generic programming scenarios.
Possible naming confusion
IFormatter<T> may be confused with ICustomFormatter or System.Runtime.Serialization.IFormatter. This is a significant naming risk and may require a different name.
Possible overlap with existing APIs
The proposal overlaps conceptually with IFormattable, ISpanFormattable, IUtf8SpanFormattable, ICustomFormatter, and delegate-based formatting.
The distinction is that IFormatter<T> is a strongly-typed external strategy interface.
Generic libraries may over-constrain APIs
Libraries may start requiring IFormatter<T> where a delegate or IFormattable would be sufficient. This is a general risk with capability interfaces.
Ambiguity around ownership of formatting rules
External formatters allow formatting behavior to be supplied outside the target type. This is useful, but it can also lead to multiple competing formatter implementations for the same type.
Performance depends on implementations
The interfaces allow allocation-free span and UTF-8 formatting, but naive implementations may allocate intermediate strings. Implementations should avoid unnecessary allocations in span and UTF-8 paths.
Compatibility commitment
Once added, interface names, method signatures, nullability annotations, variance annotations, and inheritance shape become long-term public contracts.
Open questions
-
Should the interfaces live in System, or in a more specific namespace?
-
Is IFormatter<T> the right name, or should a less ambiguous name such as ITextFormatter<T> or IValueFormatter<T> be used?
-
Should the initial proposal include only IFormatter<T>, or should it also include ISpanFormatter<T> and IUtf8SpanFormatter<T>?
-
Should IUtf8SpanFormatter<T> inherit from IFormatter<T>, or remain independent?
-
Should parser and formatter strategy interfaces be reviewed together or as separate API proposals?
Background and motivation
IFormattable,ISpanFormattable, andIUtf8SpanFormattablemodel formatting as an intrinsic capability of the value itself.That works well when the target type owns its textual representation:
However, some formatting scenarios require an external formatting strategy instead of members on the target type.
This is similar to the relationship between
IComparable<T>andIComparer<T>:The corresponding formatting relationship would be:
External formatter strategies are useful when the target type:
IFormattable,ISpanFormattable, orIUtf8SpanFormattable;There is currently no simple strongly-typed BCL strategy interface for formatting values externally.
API Proposal
API Usage
Formatting with an external strategy
Generic code that does not require the target type to implement
IFormattableExample type
The target type does not implement
IFormattable,ISpanFormattable, orIUtf8SpanFormattable.Example external formatter
Usage:
Design notes
Relationship to
IFormattableIFormatter<T>does not replaceIFormattable.IFormattablemodels an intrinsic capability of the value.IFormatter<T>models an external strategy object. Both are useful in different scenarios, just as bothIComparable<T>andIComparer<T>are useful.Relationship to
ICustomFormatterICustomFormatteris object-based and provider-oriented:IFormatter<T>would be strongly typed:This avoids boxing for value types, provides a generic type-safe contract, and composes naturally with generic code.
Why is
Tcontravariant?IFormatter<in T>only consumes values of typeT. This allows formatter strategies to be used contravariantly where appropriate.Why does
IUtf8SpanFormatter<T>not inheritISpanFormatter<T>?UTF-8 formatting can be useful independently from UTF-16 formatting. Libraries that operate directly on UTF-8 payloads may not want a UTF-16 formatting dependency in their generic constraints.
A formatter can implement both interfaces when appropriate:
Argument order
The proposed argument order is:
The value comes first because this is an external strategy operating on a supplied value. The rest of the order follows existing formatting APIs, where
formatprecedesprovider, and span formatting usesdestination,out written,format,provider.Alternative Designs
Keep using
IFormattableOne option is to require target types to implement
IFormattable,ISpanFormattable, orIUtf8SpanFormattable.This works when the target type can be modified and when formatting behavior naturally belongs to the value itself. It does not work well for third-party types, sealed types, legacy types, or multiple independent formatting strategies.
Use
ICustomFormatterICustomFormatteralready allows external formatting throughIFormatProvider.However, it is object-based rather than strongly typed. It also does not provide span-based or UTF-8 formatting contracts.
Use delegates
Another option is to use delegates such as:
or custom delegate types.
This is lightweight, but it does not provide a standard shape for format strings, culture providers, span formatting, UTF-8 formatting, or reusable formatter objects.
Use extension methods
A library can define extension methods for particular target types.
This can be convenient, but it does not provide a general generic strategy interface that can be passed around, registered, composed, or dependency-injected.
Combine parser and formatter strategies into one interface
Another option is to define a single interface that contains both parsing and formatting members.
This may be useful for round-trip conversion scenarios, but it would force types to provide both directions even when only formatting is needed. Keeping parser and formatter strategy proposals separate makes the contracts smaller and more composable.
Use a different name
The name
IFormatter<T>may be confused with existing formatting concepts such asICustomFormatterandSystem.Runtime.Serialization.IFormatter.Alternative names could include:
The proposed name follows the
IComparer<T>analogy, but the final name should be reviewed carefully.Risks
Increased public API surface area
The proposal adds new public interfaces to
System. This increases the long-term API surface area and should be justified by concrete generic programming scenarios.Possible naming confusion
IFormatter<T>may be confused withICustomFormatterorSystem.Runtime.Serialization.IFormatter. This is a significant naming risk and may require a different name.Possible overlap with existing APIs
The proposal overlaps conceptually with
IFormattable,ISpanFormattable,IUtf8SpanFormattable,ICustomFormatter, and delegate-based formatting.The distinction is that
IFormatter<T>is a strongly-typed external strategy interface.Generic libraries may over-constrain APIs
Libraries may start requiring
IFormatter<T>where a delegate orIFormattablewould be sufficient. This is a general risk with capability interfaces.Ambiguity around ownership of formatting rules
External formatters allow formatting behavior to be supplied outside the target type. This is useful, but it can also lead to multiple competing formatter implementations for the same type.
Performance depends on implementations
The interfaces allow allocation-free span and UTF-8 formatting, but naive implementations may allocate intermediate strings. Implementations should avoid unnecessary allocations in span and UTF-8 paths.
Compatibility commitment
Once added, interface names, method signatures, nullability annotations, variance annotations, and inheritance shape become long-term public contracts.
Open questions
Should the interfaces live in
System, or in a more specific namespace?Is
IFormatter<T>the right name, or should a less ambiguous name such asITextFormatter<T>orIValueFormatter<T>be used?Should the initial proposal include only
IFormatter<T>, or should it also includeISpanFormatter<T>andIUtf8SpanFormatter<T>?Should
IUtf8SpanFormatter<T>inherit fromIFormatter<T>, or remain independent?Should parser and formatter strategy interfaces be reviewed together or as separate API proposals?