Is it possible in C# to have the string formatting of an interpolated string to be performed only if it is required?
Take the following example code:
public class Program
{
private static bool isLoggingEnabled = true;
public static void Main()
{
string name = "John";
Log("Hello {0}!", name);
Log($"Hello {name}!");
}
private static void Log(string formatString, params object?[] formatStringArguments)
{
if (isLoggingEnabled)
{
Console.WriteLine(string.Format(formatString, formatStringArguments));
}
}
}
The first Log()
method call passes a format string, and so the string formatting will be performed only if the isLoggingEnabled
flag is true.
However, in the second method call that uses an interpolated string, the string formatting is performed already before the method is called, even when isLoggingEnabled
is false. It would be nice to have the string formatting be done conditionally and still benefit from the readability arising from an interpolated string.
Is it possible to annotate the formatString
and formatStringArguments
parameters of the Log()
method to indicate that they represent a format string so that the call Log($"Hello {name}!")
would be compiled into Log("Hello {0}!", name)
which would have the desired effect?
What you are after can exactly be achieved with FormattableString
like:
public void Log(FormattableString str) {
// JUST DEBUGGING
Console.WriteLine(str?.Format); // Hello {0}
Console.WriteLine(str?.GetArguments().FirstOrDefault()); // John
// formatting is not done (calling ToString)
// we just have the compiler extract a custom
// Format string and pass us a list of arguments
if (IsLoggingEnabled) {
Console.WriteLine(str.ToString());
}
}
The issue is that the compiler would prefer the overload of your other method:
Log(string formatString, params object?[] formatStringArguments)
if they are named the same. More info in this question.
So, you either have method with another name such as LogWithFormattable
or you can implement a custom interpolation handler (if on .NET 6+) which has priority over string
when overloads are done. A use case similar to yours was alluded to in this this blog post.
If we adapt slightly the code from the tutorial that was mentioned in the comments, we would get something close to what you are after:
public class Logger {
public bool IsLoggingEnabled { get; set; } = false;
// [InterpolatedStringHandlerArgument("")]
// is for the this reference of Logger
// required by the LogInterpolatedStringHandler
public void Log([InterpolatedStringHandlerArgument("")] LogInterpolatedStringHandler builder) {
if (IsLoggingEnabled) {
Console.WriteLine(builder.GetFormattedText());
} else {
// for debugging only
// will be null with current logic
Console.WriteLine(builder.GetFormattedText()); // null
}
}
public void Log(string formatString, params object?[] formatStringArguments) {
if (IsLoggingEnabled) {
Console.WriteLine(string.Format(formatString, formatStringArguments));
}
}
}
[InterpolatedStringHandler]
public struct LogInterpolatedStringHandler {
// Storage for the built-up string
StringBuilder builder;
public LogInterpolatedStringHandler(int literalLength, int formattedCount,
Logger logger,
out bool isEnabled) {
isEnabled = logger.IsLoggingEnabled;
builder = isEnabled ? new StringBuilder(literalLength) : default!;
}
public void AppendLiteral(string s) {
builder.Append(s);
}
public void AppendFormatted<T>(T t) {
builder.Append(t?.ToString());
}
internal string GetFormattedText() => builder?.ToString();
}
Simple test code:
var logger = new Logger();
logger.IsLoggingEnabled = true;
string name = "John";
logger.Log($"Hello {name}!"); // "Hello John"
what the code above translates to (i.e. the compiler magic of it all):
var logger = new Logger();
logger.IsLoggingEnabled = true;
string name = "John";
// logger.Log($"Hello {name}!"); // "Hello John"
// translates to:
bool isEnabled;
LogInterpolatedStringHandler builder = new LogInterpolatedStringHandler(7, 1, logger, out isEnabled);
// we have set the isEnabled to the value of
// logger.IsLoggingEnabled in the constructor
// we only proceed with formatting if
// isEnabled = true
if (isEnabled)
{
builder.AppendLiteral("Hello ");
builder.AppendFormatted(name);
builder.AppendLiteral("!");
}
// this finally calls our Log method
logger.Log(builder);