In this post, we’ll be looking at FormattableStringFactory
, a factory type used by .NET’s compilers to create instances of FormattableString
. We can find the FormattableStringFactory
class under the System.Runtime.CompilerServices
namespace.
In general, the class is invisible to us, and we do not usually need to interact with it. The factory’s use is not commonly required, as we have access to first-class formatting constructs in C#: concatenation, format, and interpolation.
var concat = string.Concat("one", "two");
var format = string.Format("one {0}", "two");
var two = "two";
var interpolation = $"one {two}";
So what advantages can we get using FormattableStringFactory directly? To see the benefits, we first need to look at the FormattableString
class.
The Microsoft documentation describe a FormattableString
as follows:
A composite format string consists of fixed text intermixed with indexed placeholders, called format items, that correspond to the objects in the list. The formatting operation yields a result string that consists of the original fixed text intermixed with the string representation of the objects in the list. –Microsoft
The FormattableString
implementation has several properties and methods that can provide insight into a format string before we create the string’s final value.
public abstract class FormattableString : IFormattable
{
public abstract string Format { get; }
public abstract int ArgumentCount { get; }
public abstract object?[] GetArguments();
public abstract object? GetArgument(int index);
public abstract string ToString(IFormatProvider? formatProvider);
string IFormattable.ToString(string? ignored, IFormatProvider? formatProvider)
{
return ToString(formatProvider);
}
}
Note, I’ve removed some static members for clarity.
Let’s go through each property and method. First, the Format
property is what we would expect; a string with placeholder values.
"Hello {0}"
Next, we have ArgumentCount
, which gives us the number of placeholders contained in our format string.
We then have variations that allow us to access the values of our arguments: GetArguments
and GetArgument(int index)
. We can notice that both of these methods return an object
, and this is where we see the exciting part of FormattableString
.
We know that .NET is a pass-by-reference kind of runtime, and another value can replace any object. Let’s look at an example of how we can manipulate an instance of FormattableString
.
var format =
FormattableStringFactory
.Create(
"Hello {0} {1} {2}",
"zero", "one", "two"
);
Console.WriteLine($"Format : {format.Format}");
Console.WriteLine($"# Arguments : {format.ArgumentCount}");
Console.WriteLine($"Arguments : {string.Join(",", format.GetArguments())}");
// 🧙 magic
format.GetArguments()[0] = "∞";
Console.WriteLine($"Magicked : {string.Join(",", format.GetArguments())}");
Console.WriteLine(format.ToString());
Running the code above, we get the following results.
Format : Hello {0} {1} {2}
# Arguments : 3
Arguments : zero,one,two
Magicked : ∞,one,two
Hello ∞ one two
Cool! I was able to replace one of the arguments from zero
to the infinity symbol before outputting the string value. Note that we can only replace existing placeholder arguments, and we cannot append new values as they wouldn’t match our original format string.
Conclusion
The FormattableStringFactory
and the FormattableString
classes are lovely little gems in the .NET Framework that we all utilize but likely don’t realize exists. Valuable use cases for FormattableString
might be letting users input custom message formats while still allowing us to validate properties like ArgumentCount
. The FormattableString
also allows us to replace arguments when necessary. Finally, the FormattableString
type considers culture and can appropriately format strings using the current thread’s culture.