Champion issue: #9046
In order to solve a pain point in the creation of handler types and make them more useful in logging scenarios, we add support for interpolated string handlers to receive a new piece of information, a custom value supplied at the call site.
publicvoidLogDebug(thisILoggerlogger,[InterpolatedStringHandlerArgument(nameof(logger))][InterpolatedStringHandlerArgumentValue(LogLevel.Debug)]LogInterpolatedStringHandlermessage);
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 allow a custom value to be passed along to the interpolated string handler type. The value would be specific to a particular method that uses interpolated string handler parameter. This would then permit parameterization based on the value, eliminating a large amount of duplication and making it viable to adopt interpolation handlers for ILogger
and similar scenarios.
Some examples of this:
- fedavorich/ISLE uses T4 to get around the bloat, by generating handlers for every log level.
- This BCL proposal was immediately abandoned after it was realized that there would need to be a handler type for every log level.
The compiler will recognizes the System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentValueAttribute
:
namespaceSystem.Runtime.CompilerServices{[AttributeUsage(AttributeTargets.Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassInterpolatedStringHandlerArgumentValueAttribute:Attribute{publicInterpolatedStringHandlerArgumentValueAttribute(object?value){Value=value;}publicobject?Value{get;}}}
This attribute is used on parameters, to inform the compiler how to lower an interpolated string handler pattern used in a parameter position. The attribute can be used on its own or in combination with InterpolatedStringHandlerArgument
attribute. Arrays are disallowed as argument values for the attribute in order to preserve design space.
We make one small change to how interpolated string handlers perform constructor resolution. The change is bolded below:
- The argument list
A
is constructed as follows:
- The first two arguments are integer constants, representing the literal length of
i
, and the number of interpolation components ini
, respectively.- If
i
is used as an argument to some parameterpi
in methodM1
, and parameterpi
is attributed withSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, then for every nameArgx
in theArguments
array of that attribute the compiler matches it to a parameterpx
that has the same name. The empty string is matched to the receiver ofM1
.
- If any
Argx
is not able to be matched to a parameter ofM1
, or anArgx
requests the receiver ofM1
andM1
is a static method, an error is produced and no further steps are taken.- Otherwise, the type of every resolved
px
is added to the argument list, in the order specified by theArguments
array. Eachpx
is passed with the sameref
semantics as is specified inM1
.- If
i
is used as an argument to some parameterpi
in methodM1
, and parameterpi
is attributed withSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentValueAttribute
, the attribute value is added to the argument list.- The final argument is a
bool
, passed as anout
parameter.- Traditional method invocation resolution is performed with method group
M
and argument listA
. For the purposes of method invocation final validation, the context ofM
is treated as a member_access through typeT
.
- If a single-best constructor
F
was found, the result of overload resolution isF
.- If no applicable constructors were found, step 3 is retried, removing the final
bool
parameter fromA
. If this retry also finds no applicable members, an error is produced and no further steps are taken.- If no single-best method was found, the result of overload resolution is ambiguous, an error is produced, and no further steps are taken.
// Original codevarsomeOperation=RunOperation();ILoggerlogger=CreateLogger(LogLevel.Error, ...);logger.LogWarn($"Operation was null: {operationisnull}");// Approximate translated code:varsomeOperation=RunOperation();ILoggerlogger=CreateLogger(LogLevel.Error, ...);varloggingInterpolatedStringHandler=newLoggingInterpolatedStringHandler(20,1,logger,LogLevel.Warn,outboolcontinueBuilding);if(continueBuilding){loggingInterpolatedStringHandler.AppendLiteral("Operation was null: ");loggingInterpolatedStringHandler.AppendFormatted(operationisnull);}LoggingExtensions.LogWarn(logger,loggingInterpolatedStringHandler);// Helper librariesnamespaceMicrosoft.Extensions.Logging;{usingSystem.Runtime.CompilerServices;[InterpolatedStringHandler]publicstructLoggingInterpolatedStringHandler{ public LoggingInterpolatedStringHandler(intliteralLength,intformattedCount,ILoggerlogger,LogLevellogLevel,outboolcontinueBuilding){if(logLevel<logger.LogLevel){continueBuilding=false;}else{continueBuilding=true;// Set up the rest of the builder}}}publicstaticclassLoggerExtensions{publicstaticvoidLogWarn(thisILoggerlogger,[InterpolatedStringHandlerArgument(nameof(logger))][InterpolatedStringHandlerArgumentValue(LogLevel.Warn)]refLogInterpolatedStringHandlermessage);}}
The extra attribute and an additional compiler complexity.
https://github.com/dotnet/csharplang/blob/main/proposals/interpolated-string-handler-method-names.md
None