C# Interpolated String Syntax and Usage
Understand C# interpolated string syntax, formatting options, alignment, culture handling, and performance considerations for safer, cleaner string formatting.
C# interpolated strings, introduced in C# 6, let you embed expressions directly into string literals using the $ character. Instead of concatenating values with + or using composite formatting like string.Format, you write the expression inside the braces. The compiler generates code to evaluate those expressions at runtime and format them into the final string. This makes the code easier to read and less error-prone, but there are a few details worth understanding to avoid pitfalls and get reasonable performance.
How an Interpolated String Compiles
When you write an interpolated string, the compiler does not simply produce a string at compile time. It generates code that evaluates each interpolation expression at runtime. The exact code depends on the target type and the target framework. Before .NET 6, a string target was typically compiled as a string.Format call:
string name = "Ada"; int year = 1815; string message = $"{name} was born in {year}.";
This is equivalent to the older string.Format form:
string message = string.Format("{0} was born in {1}.", name, year);
Understanding this behavior is useful because it explains why an interpolated string creates a new string object every time it is evaluated, and why the expressions are evaluated each time the line runs. It also clarifies why you can use any expression inside the braces, including method calls, property accesses, and conditional expressions, as long as the expression is valid C#.
Formatting Interpolation Expressions
You can control how an interpolated value appears by adding a format string after a colon inside the braces. The syntax is {expression:format}, and the format string follows the same rules as the format specifier in string.Format. For example:
decimal price = 19.99m; DateTime today = DateTime.Today; string pricedText = $"Price: {price:C}"; // Price: $19.99 (depending on culture) string dateText = $"Date: {today:d}"; // Date: 6/16/2025 (US culture)
The format specifier is passed to the expression's type only when that type implements IFormattable. Numeric types, DateTime, and TimeSpan do, so the common numeric and date formats work. If the type does not implement IFormattable, the format specifier is ignored. If a type such as Int32 receives an invalid format string, you can get a FormatException at runtime, so use only documented format specifiers.
Formatting uses the current culture by default. If you need a specific culture, defer the formatting and supply the provider; see the culture sections below.
Alignment and Padding
In composite formatting, you can specify a minimum width and alignment with a comma and a positive or negative number. The same applies to interpolated strings: {expression, alignment}. A negative alignment left-aligns the value, and a positive number right-aligns it. This is handy for creating aligned columns in reports or console output:
string[] names = { "Alice", "Bob", "Charlie" }; int[] scores = { 98, 85, 92 }; for (int i = 0; i < names.Length; i++) { Console.WriteLine($"{names[i],-10} {scores[i],5}"); }
The -10 left-aligns the name in a field of width 10, and 5 right-aligns the score. The alignment value is a minimum width; if the value is longer, it is not truncated.
Combining alignment and format specifier is also possible: {expression, alignment:format}. For instance, {score,10:N2} would right-align a number with two decimal places in a field of width 10.
Escaping Braces and Literal Text
To include a literal { or } character in an interpolated string, you must double it. This is because the single brace is used to delimit interpolation expressions. For example:
string braces = $"{{literal braces}} and {name}"; // Result: {literal braces} and Ada
The double braces {{ and }} produce a single brace in the output. This escaping is necessary for any literal brace, even if it is not part of an interpolation expression. This rule is easy to forget and often leads to compile-time errors, which is actually helpful because the compiler catches it.
Also note that a colon and a comma have special meanings inside the braces: the colon starts the format specifier, and the comma starts the alignment value. The common case that needs parentheses is a conditional expression, because its colon would otherwise be treated as the format separator:
string result = $"{(score >= 60 ? "pass" : "fail")}";
Deferred Formatting with FormattableString
In most code, you assign an interpolated string to a string variable or pass it to a method that expects a string, and the compiler produces a string using the current culture. However, you can also use the FormattableString type to defer formatting and control culture. The FormattableString class represents the interpolated string as a format string plus arguments, which you can convert to a string using ToString(IFormatProvider). This is useful when you want to format dates and numbers consistently across different locales. For example:
CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR"); FormattableString message = $"Price: {price:C} Date: {today:d}"; string localizedMessage = message.ToString(culture);
Here, the interpolated string is assigned to a FormattableString rather than a string. The compiler generates code that stores the format string and the arguments, allowing you to call ToString with a specific culture later. This is a powerful technique for localization, but it does carry a small overhead because it stores the arguments in an object array.
When the target type is FormattableString or IFormattable, the compiler captures the format string and arguments instead of producing a finished string. By assigning to IFormattable, you can also call ToString(string format, IFormatProvider formatProvider) directly:
IFormattable msg = $"Price: {price:C}"; string s = msg.ToString(null, culture);
Common Mistakes with Interpolated Strings
One common mistake is assuming that interpolated strings are compile-time constants. They are not, because the expression values are known only at runtime. You cannot use an interpolated string as a constant, such as in a const field or an attribute parameter. If you need a constant, use a regular string literal or concatenate string literals in a const expression.
Another common mistake is forgetting how the verbatim prefix combines with $. An interpolated string can be combined with a verbatim string by placing $ before @ (or @ before $ in newer versions; the conventional order is $@"..."). The verbatim interpolated string treats backslashes as literal characters and allows the string to span multiple lines, but braces still need escaping. For example:
string path = $@"C:\Users\{name}\Documents";
The @ makes the backslashes literal, so you do not need to double them in the file path.
Performance and Allocation Considerations
Every runtime evaluation of an interpolated string with expressions allocates a new string. Building a large report by repeatedly executing result += $"{row}\n" creates many intermediate strings. Prefer StringBuilder for that pattern:
var sb = new StringBuilder(); foreach (var row in rows) { sb.AppendLine($"{row.Name}: {row.Value}"); } string result = sb.ToString();
Using StringBuilder avoids the repeated whole-string concatenation. However, on older .NET the interpolated string passed to AppendLine is still a temporary string. If you target .NET 6 or later, StringBuilder has interpolated-string-handler overloads, so the compiler can append the formatted values directly to the builder's buffer. On older targets, AppendFormat is one way to avoid creating the line string explicitly.
In .NET 6 and later, when the target is string, the compiler uses DefaultInterpolatedStringHandler instead of a string.Format call. The handler reduces extra work and temporary allocations, but the final string is still allocated. If a method builds strings in a hot path, the handler pattern is an advanced way to avoid unnecessary allocations.
Culture and Globalization
By default, an interpolated string uses the current culture for formatting numbers and dates. That is usually correct for display, but it can be a problem for log files or API responses. To force a specific culture, use the FormattableString approach described above, or use string.Create with the culture in .NET 6+:
string s = string.Create(CultureInfo.InvariantCulture, $"{value:N2}");
The compiler recognizes the string.Create overload and passes the supplied provider to all formatting operations inside the interpolation.
Production Example: Log Entry Builder
A common practical use is building a log entry with timestamp and severity:
public void Log(LogLevel level, string message) { string entry = $"{DateTime.UtcNow:o} [{level}] {message}"; logger.WriteLine(entry); }
If you queue log entries, evaluate DateTime.UtcNow before constructing the entry if you need the exact time the entry was created.
Handling Null Values
If an interpolated expression evaluates to null, it is replaced with an empty string, not the text "null". This is often what you want. If you need a placeholder, be explicit: {value ?? "N/A"}.
When to Use Interpolated Strings vs. Alternatives
Interpolated strings are the preferred way to build short, readable strings with embedded values. They are not a replacement for a reusable format template: the expressions are evaluated and the string is built every time the line runs. If you need a template that is stored once, keep a const format string and use string.Format with it.
| Scenario | Recommended Approach |
|---|---|
| Short, one-time string with a few values | Interpolated string |
| Building many lines in a loop | StringBuilder with interpolated append |
| Formatting for a specific culture | FormattableString or string.Create |
| Reusable constant format template | const format string with string.Format |
| High-performance, many iterations | Custom interpolated string handler (only if profiling shows a need) |
Use interpolated strings by default because they are readable and safe. If profiling shows that a specific piece of code is a bottleneck, consider StringBuilder or custom handler optimization.
Advanced: Custom Interpolated String Handler
For rare cases where avoiding allocations matters, you can implement a custom type annotated with [InterpolatedStringHandler] and have the compiler pass literal parts and formatted values to it. The BCL's DefaultInterpolatedStringHandler follows the same pattern. This is an advanced optimization; most applications do not need it.
Conclusion
C# interpolated strings are a readable way to build strings with embedded expressions. They compile to code that evaluates the expressions at runtime, support format strings, alignment, culture-sensitive formatting, and perform well in modern .NET. Prefer them over manual concatenation or string.Format for most code, but understand the allocation behavior when building large outputs and use StringBuilder or a custom handler when profiling shows that it matters.