Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Explore C# string interpolation handlers
description: This advanced tutorial shows how you can write a custom string interpolation handler that hooks into the runtime processing of an interpolated string.
ms.date: 11/22/2024
ms.date: 02/05/2026
---
# Tutorial: Write a custom string interpolation handler

Expand Down Expand Up @@ -52,6 +52,9 @@ Internally, the builder creates the formatted string, and provides a member for

:::code language="csharp" source="./snippets/interpolated-string-handler/Logger-v2.cs" id="CoreInterpolatedStringHandler":::

> [!NOTE]
> When the interpolated string expression is a compile-time constant (that is, it contains no placeholders), the compiler uses the target type `string` instead of invoking a custom interpolated string handler. This means constant interpolated strings bypass custom handlers entirely.

You can now add an overload to `LogMessage` in the `Logger` class to try your new interpolated string handler:

:::code language="csharp" source="./snippets/interpolated-string-handler/Logger-v2.cs" id="LogMessageOverload":::
Expand Down Expand Up @@ -113,6 +116,9 @@ Next, you need to update the `LogMessage` declaration so that the compiler passe

This attribute specifies the list of arguments to `LogMessage` that map to the parameters that follow the required `literalLength` and `formattedCount` parameters. The empty string (""), specifies the receiver. The compiler substitutes the value of the `Logger` object represented by `this` for the next argument to the handler's constructor. The compiler substitutes the value of `level` for the following argument. You can provide any number of arguments for any handler you write. The arguments that you add are string arguments.

> [!NOTE]
> If the <xref:System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute> constructor argument list is empty, the behavior is the same as if the attribute was omitted entirely.

You can run this version using the same test code. This time, you see the following results:

```powershell
Expand Down Expand Up @@ -201,18 +207,18 @@ This example illustrates an important point for interpolated string handlers, es

:::code language="csharp" source="./snippets/interpolated-string-handler/Version_4_Examples.cs" id="TestSideEffects":::

You can see the `index` variable is incremented five times each iteration of the loop. Because the placeholders are evaluated only for `Critical`, `Error` and `Warning` levels, not for `Information` and `Trace`, the final value of `index` doesn't match the expectation:
You can see the `index` variable is incremented each iteration of the loop. Because the placeholders are evaluated only for `Critical`, `Error` and `Warning` levels, not for `Information` and `Trace`, the final value of `index` doesn't match the expectation:

```powershell
Critical
Critical: Increment index a few times 0, 1, 2, 3, 4
Critical: Increment index 0
Error
Error: Increment index a few times 5, 6, 7, 8, 9
Error: Increment index 1
Warning
Warning: Increment index a few times 10, 11, 12, 13, 14
Warning: Increment index 2
Information
Trace
Value of index 15, value of numberOfIncrements: 25
Value of index 3, value of numberOfIncrements: 5
```

Interpolated string handlers provide greater control over how an interpolated string expression is converted to a string. The .NET runtime team used this feature to improve performance in several areas. You can make use of the same capability in your own libraries. To explore further, look at the <xref:System.Runtime.CompilerServices.DefaultInterpolatedStringHandler?displayProperty=fullName>. It provides a more complete implementation than you built here. You see many more overloads that are possible for the `Append` methods.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void AppendFormatted<T>(T t)
Console.WriteLine($"\tAppended the formatted object");
}

internal string GetFormattedText() => builder.ToString();
public override string ToString() => builder.ToString();
}
// </CoreInterpolatedStringHandler>

Expand Down Expand Up @@ -61,7 +61,7 @@ public void LogMessage(LogLevel level, string msg)
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
Console.WriteLine(builder.ToString());
}
// </LogMessageOverload>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public static void Example()
for (var level = LogLevel.Critical; level <= LogLevel.Trace; level++)
{
Console.WriteLine(level);
logger.LogMessage(level, $"{level}: Increment index a few times {index++}, {index++}, {index++}, {index++}, {index++}");
numberOfIncrements += 5;
logger.LogMessage(level, $"{level}: Increment index {index++}");
numberOfIncrements++;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>interpolated_string_handler</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ public void AppendFormatted<T>(T t)
}
// </AppendWhenEnabled>

// Not part of the pattern, but needed to retrieve the formatted string
internal string GetFormattedText() => builder.ToString();
public override string ToString() => builder.ToString();
}

public enum LogLevel
Expand All @@ -67,7 +66,7 @@ public void LogMessage(LogLevel level, string msg)
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
Console.WriteLine(builder.ToString());
}
// </ArgumentsToHandlerConstructor>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ public void AppendFormatted<T>(T t, string format) where T : IFormattable
builder.Append(t?.ToString(format, null));
Console.WriteLine($"\tAppended the formatted object");
}

public void AppendFormatted<T>(T t, int alignment, string format) where T : IFormattable
{
Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with alignment {alignment} and format {{{format}}} is of type {typeof(T)},");
var formatString =$"{alignment}:{format}";
builder.Append(string.Format($"{{0,{formatString}}}", t));
Console.WriteLine($"\tAppended the formatted object");
}
// </AppendIFormattable>

public void AppendFormatted<T>(T t)
Expand All @@ -46,8 +54,7 @@ public void AppendFormatted<T>(T t)
Console.WriteLine($"\tAppended the formatted object");
}

// Not part of the pattern, but needed to retrieve the formatted string
internal string GetFormattedText() => builder.ToString();
public override string ToString() => builder.ToString();
}

public enum LogLevel
Expand All @@ -72,7 +79,7 @@ public void LogMessage(LogLevel level, string msg)
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
Console.WriteLine(builder.ToString());
}
}
}
Loading