proposals/rejected/interpolated-string-handler-method-names.md
Champion issue: https://github.com/dotnet/csharplang/issues/9046
We add support for interpolated string handlers to receive a new piece of information, the name of the method they are an argument to, in order to solve a pain point in the creation of handler types and make them more useful in logging scenarios.
public void LogDebug(
this ILogger logger,
[InterpolatedStringHandlerArgument(nameof(logger), "Method Name")] LogInterpolatedStringHandler message);
C# 10 introduced interpolated string handlers, which were intended to allow interpolated strings to
be used in high-performance and logging scenarios, using more efficient building techniques and avoiding work entirely when the
string does not need to be realized. However, a common pain point has arisen since then; for logging APIs, you will often want to
have APIs such as LogTrace, LogDebug, LogWarn, etc, for each of your logging levels. Today, there is no way to use a single
handler type for all of those methods. Instead, our guidance has been to prefer a single Log method that takes a LogLevel or
similar enum, and use InterpolatedStringHandlerArgumentAttribute to pass that value along. While this works for new APIs, the
simple truth is that we have many existing APIs that use the LogTrace/Debug/Warn/etc format instead. These APIs either must
introduce new handler types for each of the existing methods, which is a lot of overhead and code duplication, or let the calls
be inefficient. We want to introduce a small addition to the possible values in InterpolatedStringHandlerArgumentAttribute to
allow the name of the method being called to be passed along to the interpolated string handler type; this would then permit
parameterization based on the method name, eliminating a large amount of duplication and making it viable for the BCL to adopt
interpolation handlers for ILogger. Some examples of this:
We make one small change to how interpolated string handlers perform constructor resolution. The change is bolded below:
... 2. The argument list
Ais constructed as follows:
- ...
- If
iis used as an argument to some parameterpiin methodM1, and parameterpiis attributed withSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, then for every nameArgxin theArgumentsarray of that attribute the compiler matches it to a parameterpxthat has the same name. The empty string is matched to the receiver ofM1. The string"Method Name"is matched to the name ofM1.
- If any
Argxis not able to be matched to a parameter or the name ofM1, or anArgxrequests the receiver ofM1andM1is a static method, an error is produced and no further steps are taken.- Otherwise, the type of every resolved
pxis added to the argument list, in the order specified by theArgumentsarray. Eachpxis passed with the samerefsemantics as is specified inM1. If"Method Name"was present in theArgumentsarray, then a type ofstringis added to the argument list in that position.
// Original code
var someOperation = RunOperation();
ILogger logger = CreateLogger(LogLevel.Error, ...);
logger.LogWarn($"Operation was null: {operation is null}");
// Approximate translated code:
var someOperation = RunOperation();
ILogger logger = CreateLogger(LogLevel.Error, ...);
var loggingInterpolatedStringHandler = new LoggingInterpolatedStringHandler(20, 1, "LogWarn", logger, out bool continueBuilding);
if (continueBuilding)
{
loggingInterpolatedStringHandler.AppendLiteral("Operation was null: ");
loggingInterpolatedStringHandler.AppendFormatted(operation is null);
}
LoggingExtensions.LogWarn(logger, loggingInterpolatedStringHandler);
// Helper libraries
namespace Microsoft.Extensions.Logging;
{
using System.Runtime.CompilerServices;
[InterpolatedStringHandler]
public struct LoggingInterpolatedStringHandler
{
public LoggingInterpolatedStringHandler(int literalLength, int formattedCount, string methodName, ILogger logger, out bool continueBuilding)
{
var methodLogLevel = methodName switch
{
"LogDebug" => LogLevel.Debug,
"LogInfo" => LogLevel.Information,
"LogWarn" => LogLevel.Warn,
"LogError" => LogLevel.Error,
_ => throw new ArgumentOutOfRangeException(methodName),
};
if (methodLogLevel < logger.LogLevel)
{
continueBuilding = false;
}
else
{
continueBuilding = true;
// Set up the rest of the builder
}
}
}
public static class LoggerExtensions
{
public static void LogWarn(this ILogger logger, [InterpolatedStringHandlerArgument("Method Name", nameof(logger))] ref LogInterpolatedStringHandler message);
}
}
Arguably, the magic empty string that we do is already a bit of magic; we risk further complicating the feature by adding in more magic strings that users need to know.
We could design a more complicated system that allows for passing of arbitrary constants to the interpolated string handler
constructor; for example, it could be considered a bit of a hack that we use the name of the logging method, instead of a proper
LogLevel enum that the logging system likely already has. However, this would be a far more complicated language feature, would
need more BCL changes, and we don't know of any scenarios that actually need anything more than a string representing the method
name. Given this, we've opted for the simpler approach of just passing the method name.
None