C# string interpolation is the dollar-sign syntax that embeds variables and expressions directly inside a string literal. Introduced in C# 6, it has largely replaced String.Format and string concatenation in idiomatic C# code.
It has also kept evolving. C# 10 and C# 11 made it both more capable and surprisingly efficient under the hood. This post covers the full syntax, the performance story, when interpolation is the wrong tool, and the questions developers ask most.
What is C# String Interpolation?
String interpolation in C# is a compiler feature that builds a string by evaluating expressions placed inside curly braces within a string literal prefixed with a dollar sign. It was added in C# 6.0 (2015) as a more readable alternative to String.Format("{0} {1}", a, b), and over time it has also become the most efficient way to format a string in .NET. If an expression evaluates to null, the hole simply produces an empty string rather than throwing.
The minimal example:
|
1 2 3 4 |
var name = "John"; var age = 30; var message = $"Hello, my name is {name} and I am {age} years old."; Console.WriteLine(message); |
This program outputs: Hello, my name is John and I am 30 years old.
Visual Studio, VS Code and JetBrains Rider all highlight the interpolated holes inside the editor, which makes long format strings noticeably easier to scan:
The Anatomy of an Interpolated String
Almost everything in the rest of this article is a variation on a single grammar. Every interpolation hole has one mandatory part and two optional ones, always in this order:
|
1 |
$"...{expression[,alignment][:formatString]}..." |
- expression – any C# expression that returns a value. A
nullresult becomes the empty string. - alignment – a signed integer minimum field width. Positive right-aligns, negative left-aligns.
- formatString – any standard or custom format the value’s type already understands (the same vocabulary as
ToString()).
Keep those three slots straight and the alignment, formatting and culture sections below all fall into place.
Embedding Expressions, Not Just Variables
What goes between the braces of an interpolated string is a full C# expression, not just an identifier. You can call methods, do arithmetic, index into an array, or compose anything that produces a value:
|
1 2 3 4 |
var price = 29.99; var taxRate = 0.08; var total = $"Total cost: {price * (1 + taxRate)}"; Console.WriteLine(total); |
This program outputs: Total cost: 32.3892
Conditional (Ternary) Expressions Inside an Interpolated String
The ternary operator works fine inside a hole, but it has to be wrapped in parentheses – otherwise the compiler reads the first ? as the optional-format separator and bails out with a cryptic error. This is by far the most common mistake I see when developers reach for branching inside an interpolated string:
|
1 2 3 |
var userScore = 1200; var scoreMessage = $"Your score is {userScore} and you are {(userScore > 1000 ? "above" : "below")} average."; Console.WriteLine(scoreMessage); |
Formatting Numbers, Currencies and Dates
After an expression, the colon : introduces a standard or custom .NET format specifier – the same vocabulary used by ToString() and String.Format.
A custom numeric format:
|
1 2 3 |
var pi = Math.PI; var formattedPi = $"Pi to three decimal places: {pi:.###}"; Console.WriteLine(formattedPi); |
Output: Pi to three decimal places: 3.142
A standard currency format:
|
1 2 3 4 |
var price = 29.99; var taxRate = 0.08; var total = $"Total cost: {price * (1 + taxRate):C}"; Console.WriteLine(total); |
Output: Total cost: $32.29 (under an en-US culture).
A custom date format:
|
1 2 |
DateTime now = DateTime.Now; Console.WriteLine($"Current date: {now:yyyy-MMM-dd}"); |
Output: Current date: 2026-May-14
The format specifiers you reach for most often are worth memorizing:
{x:C}– currency, e.g.$1,234.50{x:N2}– number with thousands separators and 2 decimals, e.g.1,234.50{x:F2}– fixed-point with 2 decimals, no separators, e.g.1234.50{x:P1}– percent with 1 decimal, e.g.12.3 %{x:X}– hexadecimal for integers, e.g.FF{d:yyyy-MM-dd}– ISO-style date, e.g.2026-05-25
One thing worth remembering: interpolation uses the current thread’s culture. If the result will be parsed later – a JSON number, a file path, a CSV cell – use FormattableString.Invariant($"...") instead of relying on the default. Cross-locale bugs caused by a stray comma decimal separator are a classic gotcha (more on that below).
Aligning Values with the Comma Syntax
A comma , after the expression specifies the minimum field width. A positive number right-aligns, a negative number left-aligns:
|
1 2 3 4 |
var item = "apple"; var quantity = 5; Console.WriteLine($"Item: {item,10} | Quantity: {quantity,3}"); Console.WriteLine($"Item: {item,-10} | Quantity: {quantity,-3}"); |
This program outputs:
|
1 2 |
Item: apple | Quantity: 5 Item: apple | Quantity: 5 |
Beware: if the value is wider than the field, the runtime does not truncate it. One oversized value silently breaks the alignment of every following row, which is why ad-hoc tables built this way always end up looking ragged in production logs. For real tabular output, validate widths yourself or use a dedicated table-formatting library.
You can combine alignment and format in the same hole: {value,10:N2} reserves ten characters and formats with two decimal places.
New Lines Inside an Interpolated String (C# 11)
Since C# 11, you can put line breaks inside an interpolation hole. The rule applies to the expression, not the literal portion, but it makes interpolated strings that contain a pattern match or a multi-line LINQ query dramatically more readable:
|
1 2 3 4 5 6 7 8 |
string message = $"At {degreeCelsius}°C, the state of H2O is: { degreeCelsius switch { >= 100 => "Steam - Water has boiled and turned to vapor.", >= 0 => "Liquid - Water is in its liquid state.", _ => "Ice - Water has frozen and turned to solid ice.", } }"; |
To break the literal text across lines you still need a verbatim ($@"...") or raw ($"""...""") string.
Escaping Curly Braces (and Raw String Interpolation)
To include a literal curly brace in the output, double the character:
|
1 2 3 |
var count = 3; var message = $"{{This message includes {count} curly braces}}."; Console.WriteLine(message); |
Output: {This message includes 3 curly braces}.
This doubling rule is inherited from String.Format and quickly becomes painful when the string you’re building is itself JSON, XML, or C# source code full of braces. C# 11 fixed that with raw string literals: prefix with $$ instead of $ and the compiler treats only double braces as interpolation holes. Single braces stay literal – no escaping required:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
string sister = "Léna"; string brother = "Paul"; Console.ForegroundColor = ConsoleColor.White; Console.BackgroundColor = ConsoleColor.DarkBlue; Console.WriteLine( $$""" { "sister": "{{sister}}", "brother": "{{brother}}", } """); Console.BackgroundColor = ConsoleColor.Black; Console.ReadKey(); |
This program outputs:
The number of $ signs you can stack is unbounded. Three dollar signs means triple braces are the interpolation markers, and so on. This lets an interpolated string contain any sequence of consecutive braces verbatim:
Interpolated Strings as Constants (C# 10)
A small but handy C# 10 addition: if every hole in an interpolated string is itself a constant, the whole thing can be a const string. This lets you compose readable constants without dropping back to concatenation:
|
1 2 3 |
const string Product = "NDepend"; const string Version = "2024"; const string Banner = $"{Product} {Version}"; // valid since C# 10 |
Before C# 10 this was a compile error, and only plain string literals could be constants. The expressions still have to be compile-time constants – a method call or a non-const variable in any hole pushes the result back to a regular runtime string.
Controlling Culture: Invariant vs Culture-Specific Output
By default an interpolated string formats every value with CultureInfo.CurrentCulture. That is exactly what you want for text shown to a user, and exactly what you do not want for machine-readable output, because the decimal separator, date order and digit grouping all change with the locale.
Since .NET 6 the cleanest way to pin the culture is the string.Create(IFormatProvider, ...) overload, which accepts the interpolated string directly:
|
1 2 3 4 5 6 7 |
using System.Globalization; decimal price = 1234.5m; string invariant = string.Create(CultureInfo.InvariantCulture, $"{price:N2}"); string german = string.Create(CultureInfo.GetCultureInfo("de-DE"), $"{price:N2}"); // invariant -> 1,234.50 // german -> 1.234,50 |
For the very common “I just need invariant” case, FormattableString.Invariant($"...") is the shorter form and works on every framework since C# 6. Reach for one of these any time the string ends up in JSON, a URL, a SQL literal, a file name or a log that another program will parse.
How C# String Interpolation Compiles: the Performance Story
Before C# 10 and .NET 6, an interpolated string was syntactic sugar over String.Format. Concretely, this program:
|
1 2 |
string str = $"Welcome back, {name}! Today is {DateTime.Now:MMMM dd}, and it's a perfect day for coding."; |
…compiled into this:
|
1 2 3 4 |
string str = string.Format( "Welcome back, {0}! Today is {1:MMMM dd}, and it's a perfect day for coding.", name, DateTime.Now); |
That meant the format string was re-parsed at runtime on every call, the arguments were boxed into an object[], and an intermediate StringBuilder was allocated. Fine for an occasional log line, expensive in a hot path.
Since C# 10 and .NET 6, the same interpolated string compiles to something quite different:
|
1 2 3 4 5 6 7 |
DefaultInterpolatedStringHandler x = new DefaultInterpolatedStringHandler(61, 2); x.AppendLiteral("Welcome back, "); x.AppendFormatted(name); x.AppendLiteral("! Today is "); x.AppendFormatted(DateTime.Now, "MMMM dd"); x.AppendLiteral(", and it's a perfect day for coding."); string str = x.ToStringAndClear(); |
The format string is now parsed once, at build time. There is no object[], value types are appended through a generic AppendFormatted<T> overload that avoids boxing, and the working buffer is rented from an array pool instead of being freshly allocated. In Stephen Toub’s announcement the rewrite measured roughly a 40% throughput improvement and almost a 5x reduction in allocations against the old String.Format path – which is why “interpolation is slower” is now outdated advice.
ISpanFormattable: Eliminating the Last Temporary Allocations
One allocation the new handler couldn’t yet remove was the intermediate string each value produced when ToString() was called on it. Take this small type:
|
1 2 3 |
public readonly record struct Point(int X, int Y) { public override string ToString() => $"({X}, {Y})"; } |
Until recently, formatting that Point via interpolation allocated two short-lived strings – one for X, one for Y – and copied them into the final buffer.
The ISpanFormattable interface, available since .NET 10 on the interpolation path, lets a type write its characters directly into the destination Span<char> rather than producing an intermediate string. All BCL primitive types (int, double, DateTime, Guid, …) implement it, and custom types can too. The net result is that an interpolated string made entirely of ISpanFormattable values can be produced with a single heap allocation – the final string itself.
For an in-depth coverage of the compiler interpolated string handler design, Stephen Toub’s String Interpolation in C# 10 and .NET 6 is still the canonical reference.
When You Should NOT Use C# String Interpolation
For all its convenience, interpolation is the wrong tool in two specific situations.
Building SQL or shell commands. An interpolated string concatenates the values you give it as text. Inside a SQL query that becomes a textbook SQL injection vulnerability. Always use parameterized queries, or the SQL-specific FormattableString overloads (for example the EF Core FromSqlInterpolated method, which routes the holes into parameters instead of into the SQL text).
Logging. A call like logger.LogInformation($"Processed user {userId}") performs the formatting work even when the log level is filtered out – wasted CPU and allocations on every call. Worse, the structured-logging pipeline never sees userId as a discrete field; it sees one already-formatted message. Use the {Placeholder} message-template syntax of your logger instead: logger.LogInformation("Processed user {UserId}", userId).
Outside of those two cases, reach for the dollar sign. The readability win is real, the compiler now catches typos and missing values, and as we saw above the runtime cost is essentially nil.
String Interpolation vs String.Format vs Concatenation
The same output can usually be expressed three ways in C#. Here is how they compare on the criteria that matter day to day:
| Approach | Readability | Compile-time checking | Performance on .NET 6+ | Best for |
|---|---|---|---|---|
Interpolation $"...{x}..." |
Highest | Yes – a missing variable is a build error | Fastest – direct handler calls, no boxing | Almost everything |
String.Format("{0}", x) |
Medium | No – index mistakes surface at runtime | Slower – format string parsed on every call | Format strings that come from a resource file or config |
Concatenation "x = " + x |
Low past 2-3 parts | Partial | Fine for a couple of short pieces | Joining two or three short values |
If you maintain older code from before C# 6, converting String.Format calls into interpolated strings is one of the safer modernization refactorings. Both Visual Studio and Rider offer it as a one-click code fix.
C# String Interpolation FAQ
Which C# version added string interpolation?
String interpolation shipped in C# 6.0 in 2015. Constant interpolated strings arrived in C# 10, and multi-line holes plus raw-string interpolation came with C# 11.
Is string interpolation slower than String.Format in C#?
No. Since C# 10 and .NET 6 it is generally faster, because the compiler parses the format string once at build time and emits direct calls into DefaultInterpolatedStringHandler. On older targets such as .NET Framework or netstandard2.0 it still lowers to String.Format, so the two are equivalent there.
What happens if an interpolated value is null?
Nothing breaks. A hole that evaluates to null produces the empty string, exactly as String.Format does, so you never get a NullReferenceException from interpolation itself.
How do I format a number to two decimal places?
Use the format component after a colon: $"{value:F2}" for plain fixed-point (1234.50) or $"{value:N2}" to add thousands separators (1,234.50).
Can an interpolated string be a const?
Since C# 10, an interpolated string whose holes are themselves constants is a valid const string. Before that, only plain string literals could be constants.
How do I make an interpolated string culture-invariant?
Use string.Create(CultureInfo.InvariantCulture, $"...") on .NET 6+, or the shortcut FormattableString.Invariant($"..."), or assign to a FormattableString and call .ToString(CultureInfo.InvariantCulture). This is critical for any output that will be parsed later (JSON numbers, decimal file names, machine-readable logs).
What is the difference between $@"..." and $$"""..."""?
$@"..." combines verbatim (no escape sequences, multi-line) with interpolation, and is still convenient for short Windows file paths. The $ and @ can appear in either order. $$"""...""" is the C# 11 raw-string variant; it additionally lets the content contain quotes and braces without escaping, which is what you want when the output is JSON, regex or embedded source code.
Does string interpolation protect against SQL injection?
No. $"SELECT * FROM Users WHERE Name = '{name}'" is just as dangerous as plain concatenation. Always use parameterized queries.
Can I interpolate into a StringBuilder?
Yes – since .NET 6 there is a StringBuilder.Append(ref AppendInterpolatedStringHandler) overload that lets you write sb.Append($"x = {x}") with no intermediate string allocated.
Does string interpolation work in .NET Framework?
Yes. The syntax has been available since C# 6, regardless of runtime. On .NET Framework and netstandard2.0 it compiles down to String.Format rather than to the modern handler, so you get the readability but not the .NET 6+ allocation savings.
Conclusion
C# string interpolation hits a sweet spot that very few language features manage: it is easier to read than the alternatives, the compiler catches mistakes the older APIs accepted silently, and the runtime cost has been driven down to the point where you no longer trade readability for performance. The one rule worth keeping in mind is the boundary case – never reach for the dollar sign when you’re building SQL, shell commands, or log messages that carry structured fields. Everywhere else, the modern dollar-sign syntax is now the right default in idiomatic C# code.



