Back to Blog
C#

C# String Interpolation Formatting: Syntax and Specifiers

c# string interpolation formatting: Learn how to control alignment, format specifiers, and culture in C# string interpolation, plus common pitfalls and performance con...

C#String InterpolationFormat Specifiers.NETCultureString Formatting
Diagram showing C# string interpolation with format specifiers and alignment, illustrating how values are inserted into a template.

C# string interpolation formatting lets you embed expressions directly into a string template using the $ prefix. The compiler converts each interpolated expression into a call to string.Format or a similar formatting mechanism, but the syntax is more readable and less error-prone than passing format arguments separately. This article covers the formatting options available inside interpolated strings, including alignment, format specifiers, culture handling, and common pitfalls.

Basic Interpolation Syntax and How It Works

The simplest interpolated string contains a variable or expression inside braces:

string name = "Ada"; int year = 1843; Console.WriteLine($"{name} wrote the first algorithm in {year}.");

Each {expression} is replaced by the string representation of the value, using the current culture's rules for numbers, dates, and other types. The compiler translates this into a call that behaves like string.Format("{0} wrote the first algorithm in {1}.", name, year), but the inline syntax keeps the template and arguments together, which reduces the chance of mismatched placeholders.

You can also use any valid C# expression inside the braces, including method calls, property access, and ternary operators. For example:

double price = 19.99; Console.WriteLine($"Price: {price * 1.08:C}");

Here the expression price * 1.08 is evaluated first, then formatted with the C (currency) format specifier.

Controlling Alignment and Width

When you need to align values within a fixed-width column, you can add an alignment component after the expression, separated by a comma. The syntax is {expression, alignment} where alignment is a positive integer for right-alignment or a negative integer for left-alignment. The value is padded with spaces to the specified width.

string[] names = { "Alice", "Bob", "Carol" }; int[] scores = { 95, 82, 91 }; for (int i = 0; i < names.Length; i++) { Console.WriteLine($"{names[i],-10}{scores[i],5}"); }

This prints each name left-aligned in a 10-character field and each score right-aligned in a 5-character field. Alignment is especially useful for tabular output in console applications or log files. If the value is longer than the alignment width, the alignment is ignored and the full value is printed.

Alignment works independently of format specifiers. You can combine both: {expression,alignment:format}. For example, {price,10:C2} right-aligns the currency value in a 10-character field and formats it with two decimal places.

Using Format Specifiers for Numbers, Dates, and More

Format specifiers control how a value is converted to text. They are placed after a colon inside the braces: {expression:format}. The set of valid specifiers depends on the type of the expression. Numeric types support standard numeric format strings like N (number with group separators), F (fixed-point), P (percent), and C (currency). Dates and times support patterns such as yyyy-MM-dd or HH:mm.

double ratio = 0.125; Console.WriteLine($"{ratio:P1}"); // 12.5% DateTime now = DateTime.Now; Console.WriteLine($"{now:yyyy-MM-dd HH:mm}");

For custom numeric formats, you can use # and 0 placeholders. For example, {value:#,##0.00} forces a thousands separator and two decimal places. The exact output depends on the current culture, which determines decimal separators, group sizes, and currency symbols.

When you need a format specifier that is not predefined, you can implement IFormattable on your own types. The ToString(string format, IFormatProvider formatProvider) method receives the specifier and the active culture, allowing you to define custom formatting behavior that works with interpolation.

Culture-Sensitive Formatting and the Current Culture

By default, interpolated strings use the current culture (CultureInfo.CurrentCulture). This is appropriate for user-facing output in desktop or web applications, but it can cause problems in contexts that require a fixed culture, such as generating data files, constructing URLs, or producing logs that must be parsed consistently.

To force a specific culture, you can use the FormattableString class. An interpolated string can be assigned to a FormattableString variable, and then you can call ToString(IFormatProvider) with the desired culture:

FormattableString message = $"Value: {value:N2}"; string invariant = message.ToString(CultureInfo.InvariantCulture);

This works because the compiler generates a FormattableString when the target type is known. The same technique lets you pass the interpolated string to a method that accepts IFormatProvider.

In .NET 6 and later, you can also use the string.Create(IFormatProvider, DefaultInterpolatedStringHandler) overload to control culture without an intermediate FormattableString. This is more efficient because it writes directly into the target string buffer, but the FormattableString approach is simpler and works on all .NET versions that support interpolation.

Common Pitfalls: Escaping Braces and Null Values

Two frequent mistakes often trip up developers new to interpolation. First, to include a literal brace character in the output, you must double it: {{ for a left brace and }} for a right brace. The compiler interprets single braces as the start of an interpolation expression.

Console.WriteLine($"{{ {name} }}"); // prints { Ada }

Second, if an interpolated expression evaluates to null, the result is an empty string rather than the text "null". This is the same behavior as string.Format. If you need to display a placeholder for null, use the null-coalescing operator:

string? city = null; Console.WriteLine($"City: {city ?? "unknown"}");

Another subtle issue arises when you use a conditional expression inside interpolation. The : character is already used for format specifiers, so you must wrap the conditional in parentheses:

int score = 75; Console.WriteLine($"{(score >= 60 ? "Pass" : "Fail")}");

Without the parentheses, the compiler interprets : as the start of a format specifier and raises an error.

Performance Considerations: Interpolation vs Composite Formatting

Interpolated strings are convenient, but they are not always the most efficient way to build large or frequently executed strings. Each interpolation expression is boxed if the value is a value type, and the resulting string is allocated on the heap. For a single log message or a UI label, this overhead is negligible. In hot paths that format thousands of messages per second, the allocation cost can become measurable.

In .NET 6 and later, the compiler can use DefaultInterpolatedStringHandler to avoid intermediate allocations. When you assign an interpolated string to a string, the handler writes directly into a StringBuilder-like buffer and returns the final string. This reduces the number of temporary strings created, but it still allocates the final string.

For scenarios that require repeated formatting with a changing set of values, a reusable StringBuilder with AppendFormat may be more efficient because it avoids creating a new string for each intermediate step. However, StringBuilder requires you to manage format placeholders manually, which is more error-prone.

If you need to format many rows in a loop, consider whether you can use a single interpolated string per row or whether a StringBuilder with AppendLine is clearer. The performance difference is rarely significant unless the loop runs millions of times or the strings are very large. Profile your application before optimizing, and prefer readability in most cases.

When to Choose Interpolation Over string.Format or StringBuilder

Interpolation is the best default for most string construction because it keeps the template and values together, making the code easier to read and maintain. Use string.Format when you already have a format string stored in a variable or configuration, because interpolation requires the template to be a compile-time constant. For example, a localization system might retrieve a format string from a resource file; in that case, string.Format is the appropriate tool.

Use StringBuilder when you are building a large string incrementally across many statements, such as assembling a CSV file or a multi-line report. Interpolation works well for a single line, but chaining many interpolated strings with + creates multiple intermediate strings. StringBuilder.Append or AppendLine avoids that overhead.

A hybrid approach is also valid: use StringBuilder for the overall structure and call AppendLine($"...") for each row. This gives you the readability of interpolation while avoiding the quadratic behavior of repeated string concatenation. The decision ultimately depends on the size of the output, the number of formatting operations, and whether the format template is known at compile time.