What’s New in C# 10: Take Control of Interpolated String Handling

This is part of a series on the new features introduced with C# 10.

In C# you can create an interpolated string such as: $"{DateTime.Now}: starting..."

The compiler will transform this to a single string instance using a call to String.Format or String.Concat.

Starting with C# 10 you can override this behaviour if you want more control such as:

  • Not interpolating the sting for performance reasons if it won’t be used
  • Limiting the length of resulting interpolated strings
  • Enforcing custom formatting of interpolated strings
  • Etc.

Take the following simple logging class:

// Simplified implementation
public static class SimpleConsoleLogger
{
    public static bool IsLoggingEnabled { get; set; }

    public static void Log(string message)
    {
        if (IsLoggingEnabled)
        {
            Console.WriteLine(message);
        }            
    }
}

We could call this as follows:

SimpleConsoleLogger.IsLoggingEnabled = true;
SimpleConsoleLogger.Log($"{DateTime.Now}: starting...");
SimpleConsoleLogger.IsLoggingEnabled = false;
SimpleConsoleLogger.Log($"{DateTime.Now}: ending...");

The second call (SimpleConsoleLogger.Log($"{DateTime.Now}: ending...");) won’t output a log message because IsLoggingEnabled is false, however the interpolation of the string $"{DateTime.Now}: ending..." will still take place.

Ideally if logging is not enabled we would not even want to bother interpolating the string. This could improve the performance of the application if logging was identified as a problem.

We can do this by taking control of when (or if) an interpolated string is processed by:

  • Applying the System.Runtime.CompilerServices.InterpolatedStringHandler attribute to a custom handler
  • Creating a constructor with int parameters: (int literalLength, int formattedCount)
  • Adding a public AppendLiteral method
  • Adding a generic public AppendFormatted method

Within this custom handler you can decide how to turn the interpolated string into a single string instance, for example by using a StringBuilder. In the code you could also enforce any custom formatting/length restrictions that are required.

The following code shows a simple example using a StringBuilder:

using System.Runtime.CompilerServices;
using System.Text;

namespace ConsoleApp1
{
    [InterpolatedStringHandler]
    public ref struct LogMessageInterpolatedStringHandler
    {
        readonly StringBuilder logMessageStringbuilder;
     
        public LogMessageInterpolatedStringHandler(int literalLength, int formattedCount)
        {
            logMessageStringbuilder = new StringBuilder(literalLength);
        }

        public void AppendLiteral(string s)
        {
            // For demo purposes
            Console.WriteLine($"AppendLiteral called for '{s}'");

            logMessageStringbuilder.Append(s);
        }

        public void AppendFormatted<T>(T t)
        {
            // For demo purposes
            Console.WriteLine($"AppendFormatted called for '{t}'");

            logMessageStringbuilder.Append(t?.ToString());
        }

        public string BuildMessage() => logMessageStringbuilder.ToString();
    }
}

To make the logging class use this we can add another overload of the log method that instead of a string takes a LogMessageInterpolatedStringHandler:

public static void Log(LogMessageInterpolatedStringHandler logMessageBuilder)
{
    if (IsLoggingEnabled)
    {
        Console.WriteLine("...interpolating message because logging is enabled...");
        Console.WriteLine(logMessageBuilder.BuildMessage());
    }
    else
    {
        Console.WriteLine("...NOT interpolating message because logging is disabled...");
    }
}

Now if Log is called with a non-interpolated string like "Hello - this is not an interpolated string" the original log method will be used.

If the Log method is called with an interpolated string, the custom handler will be invoked (if we choose to invoke it). For example, if logging is disabled we don’t even need to call the handler to build the final log message:

public static void Log(LogMessageInterpolatedStringHandler logMessageBuilder)
{
    if (IsLoggingEnabled)
    {
        Console.WriteLine("...interpolating message because logging is enabled...");
        Console.WriteLine(logMessageBuilder.BuildMessage());
    }
    else
    {
        Console.WriteLine("...NOT interpolating message because logging is disabled...");
    }
}

The final code looks like this:

namespace ConsoleApp1
{
    // Simplified implementation
    public static class SimpleConsoleLogger
    {
        public static bool IsLoggingEnabled { get; set; }

        public static void Log(string message)
        {
            Console.WriteLine("...may have already interpolated the message...");

            if (IsLoggingEnabled)
            {
                Console.WriteLine(message);
            }            
        }

        public static void Log(LogMessageInterpolatedStringHandler logMessageBuilder)
        {
            if (IsLoggingEnabled)
            {
                Console.WriteLine("...interpolating message because logging is enabled...");
                Console.WriteLine(logMessageBuilder.BuildMessage());
            }
            else
            {
                Console.WriteLine("...NOT interpolating message because logging is disabled...");
            }
        }

    }
}


using System.Runtime.CompilerServices;
using System.Text;

namespace ConsoleApp1
{
    [InterpolatedStringHandler]
    public ref struct LogMessageInterpolatedStringHandler
    {
        readonly StringBuilder logMessageStringbuilder;
     
        public LogMessageInterpolatedStringHandler(int literalLength, int formattedCount)
        {
            logMessageStringbuilder = new StringBuilder(literalLength);
        }

        public void AppendLiteral(string s)
        {
            // For demo purposes
            Console.WriteLine($"AppendLiteral called for '{s}'");

            logMessageStringbuilder.Append(s);
        }

        public void AppendFormatted<T>(T t)
        {
            // For demo purposes
            Console.WriteLine($"AppendFormatted called for '{t}'");

            logMessageStringbuilder.Append(t?.ToString());
        }

        public string BuildMessage() => logMessageStringbuilder.ToString();
    }
}


SimpleConsoleLogger.IsLoggingEnabled = true;
SimpleConsoleLogger.Log($"{DateTime.Now}: starting...");
SimpleConsoleLogger.Log("Hello - this is not an interpolated string");
SimpleConsoleLogger.IsLoggingEnabled = false;
SimpleConsoleLogger.Log($"{DateTime.Now}: ending...");

And if we run this:

AppendFormatted called for '30/11/2021 11:52:02 AM'
AppendLiteral called for ': starting...'
...interpolating message because logging is enabled...
30/11/2021 11:52:02 AM: starting...
...may have already interpolated the message...
Hello - this is not an interpolated string
AppendFormatted called for '30/11/2021 11:52:02 AM'
AppendLiteral called for ': ending...'
...NOT interpolating message because logging is disabled...

Now the performance overhead of interpolating the strings will only happen if logging is enabled.

There’s a much more in-depth tutorial in the docs.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Add comment

Loading