CSharp 10 Features

Last Updated: 4/8/2022

Custom String Interpolation Handler

  • When using string interpolation, the default interpolated string handler process placeholders similar to String.Format. Each placeholder is formatted as text, and then the components are concatenated to form the resulting string.
  • C# 10 adds support for a custom interpolated string handler.
  • An interpolated string handler is a type that processes the placeholder expression in an interpolated string.

Custom Handler

To implement custom interpolated string handler follow these steps

  • System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute applied to the type.
  • A constructor that has two int parameters, literalLength and formatCount. (More parameters are allowed).
  • A public AppendLiteral method with the signature
  • A generic public AppendFormatted method
[InterpolatedStringHandler]
public struct LogInterpolatedStringHandler
{
	StringBuilder builder;
	public LogInterpolatedStringHandler(int literalLength, int formattedCount) 
	{
		builder = new StringBuilder(literalLength);			
	}
	
	public void AppendLiteral(string s) 
	{
		Console.WriteLine($"Append Literal {s}");
		builder.Append(s);
	}
	
	public void AppendFormatted<T>(T t)
	{
		Console.WriteLine($"Append Formatted {t}");
		builder.Append(t.ToString());
	}
	
	internal string GetFormattedText() => builder.ToString();
}

A simple logger class that logs the message to console. When interpolated string is passed, the method with interpolated string handler is executed

public class Logger
{
    public void LogMessage(string msg)
    {
		Console.WriteLine("LogMessage string version");
        Console.WriteLine(msg);
    }
	
	public void LogMessage(LogInterpolatedStringHandler builder)
    {
		Console.WriteLine("LogMessage interpolated string version");
        Console.WriteLine(builder.GetFormattedText());
    }
}

var logger = new Logger();
logger.LogMessage("Critical messsage");
Console.WriteLine("");
logger.LogMessage($"Critical messsage at {DateTime.Now}");

Pass Additional Parameters

You can pass additional parameters to Interpolated String Handler

public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel)
{
}

Update the method so that compiler passes additional parameter to the constructor parameter

public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
    Console.WriteLine(builder.GetFormattedText());
}

InterpolatedStringHandlerArgument specifies the list of arguments that follow the required literalLength and formattedCount parameters in the constructor.

The compiler substitutes the value of the Logger object represented by this for the logger parameter in the constructor. The compiler substitutes the value of level for the logLevel parameter in the constructor.

In this example, you will log the message only if the EnabledLevel is greater than parameter loglevel. Also the process to construct the string from interpolated string expression is not done if the EnabledLevel is lesser than logLevel

public enum LogLevel
{
    Off,
    Critical,
    Error,
    Warning,
    Information,
    Trace
}
[InterpolatedStringHandler]
public struct LogInterpolatedStringHandler
{
	StringBuilder builder;
	private readonly bool enabled;
	public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel) 
	{
		enabled = logger.EnabledLevel >= logLevel;
		builder = new StringBuilder(literalLength);			
	}
	
	public void AppendLiteral(string s) 
	{
		if(!enabled) return;
		Console.WriteLine($"Append Literal {s}");
		builder.Append(s);
	}
	
	public void AppendFormatted<T>(T t)
	{
		if(!enabled) return;
		Console.WriteLine($"Append Formatted {t}");
		builder.Append(t.ToString());
	}
	
	internal string GetFormattedText() => builder.ToString();
}


public class Logger
{
    public LogLevel EnabledLevel { get; init; } = LogLevel.Error;

    public void LogMessage(LogLevel level, string msg)
    {
		Console.WriteLine("LogMessage string version");
        if (EnabledLevel < level) return;
        Console.WriteLine(msg);
    }
	
	public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")]LogInterpolatedStringHandler builder)
    {
		Console.WriteLine("LogMessage interpolated string version");
        if (EnabledLevel < level) return;
        Console.WriteLine(builder.GetFormattedText());
    }
}

var logger = new Logger();
logger.LogMessage(LogLevel.Critical, "Critical messsage");
Console.WriteLine("");
logger.LogMessage(LogLevel.Critical, $"Critical messsage {DateTime.Now}");
Console.WriteLine("");
logger.LogMessage(LogLevel.Information, $"Information messsage {DateTime.Now}");

//Output
/*
LogMessage string version
Critical messsage

Append Literal Critical messsage 
Append Formatted 04/08/2022 09:47:35
LogMessage interpolated string version
Critical messsage 04/08/2022 09:47:35

LogMessage interpolated string version
*/

Example

References:

https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler