Good day!

I'm experimenting with some logging features, in particular adding caller information for logging using interpolated strings. This is in Visual Studio 2022 using C# & .NET 6.0.

I'm trying to add the caller information (name, path, and line number) to the method:

public static void Log<T>(this T logLabel, [InterpolatedStringHandlerArgument("logLabel")] TraceLoggerParamsInterpolatedStringHandler handler) 
    where T : Enum
{
        ...
}

The dilemma is the caller information needs to occur before the handler for those arguments to be sent to the constructor of the TraceLoggerParamsInterpolatedStringHandler itself. However, because those attributes require those parameters to have default values, that means the handler requires a default value, too.

If I do:

public static void Log<T>(this T logLabel,
    [CallerMemberName] string callerName = "",
    [CallerFilePath] string callerFilePath = "",
    [CallerLineNumber] int callerLineNumber = -1,
    [InterpolatedStringHandlerArgument("logLabel", "callerName", "callerFilePath", "callerLineNumber")] TraceLoggerParamsInterpolatedStringHandler handler = default) 
    where T : Enum
{
        ...
}

Then the handler is initialized as the default, it doesn't call the specialized constructor that takes the additional parameters. I don't know if this is a code-gen issue in this case for interpolated strings.

Here is the disasm for the call site:

00007FF84DD2B2AF  call        Method stub for: System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(Int32, Int32) (07FF84D6AC1B0h)  
00007FF84DD2B2B4  mov         rdx,260C53C4D68h  
00007FF84DD2B2BE  mov         rdx,qword ptr [rdx]  
00007FF84DD2B2C1  lea         rcx,[rbp+0A8h]  
00007FF84DD2B2C8  call        Method stub for: System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendLiteral(System.String) (07FF84D6AC1F8h)  

I'm suspecting however I'll just have to move those parameters after and pass them to the handler explicitly to work around this.

If there is a more elegant solution, I'd appreciate the insight!

Thank you,

Brent Scriver

1

There are 1 best solutions below

0
On

Turns out this is pretty simple: just add the various [Caller...] arguments to your InterpolatedStringHandler constructor itself. Like this:

[InterpolatedStringHandler]
public readonly ref struct LogInterpolatedStringHandler
{
    private readonly StringBuilder _builder;

    public LogInterpolatedStringHandler(int literalLength, int formattedCount,
        [CallerFilePath] string filePath = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        _builder = new StringBuilder(literalLength);

        _builder.Append(filePath);
        _builder.Append(':');
        _builder.Append(lineNumber);
    }

    public void AppendLiteral(string s)
    {
        _builder.Append(s);
    }

    public void AppendFormatted<T>(T t)
    {
        _builder.Append(t);
    }

    internal string GetFormattedText() => _builder.ToString();
}

Then, you don't need the [InterpolatedStringHandlerArguments] at all. Your method signature can just be:

static void Log(LogInterpolatedStringHandler handler)
{
    Console.WriteLine(handler.GetFormattedText());
}

And you call it as expected:

static void Main(string[] args)
{
    string s = "Hello World";
    int i = 50;
    Log($"string is {s}, int is {i}");
}

The compiler, correctly, turns that into this:

private static void Main(string[] args)
{
    string s = "Hello World";
    int i = 50;
    LogInterpolatedStringHandler handler = new LogInterpolatedStringHandler(19, 2, "<full path here>\\Program.cs", 87);
    handler.AppendLiteral("string is ");
    handler.AppendFormatted(s);
    handler.AppendLiteral(", int is ");
    handler.AppendFormatted(i);
    Log(handler);
}

And, by the way, this will still work if you actually do have to use [InterpolatedStringHandlerArgument] with extra arguments.