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);
Logmethod, see thatisLoggingEnabledis false and not bother constructing the formatted string, so there will be no performance hit in that case. As ever with performance, profile to find your hotspots and only worry about those.trueorfalse)