Raw string literals: syntax, indentation rules, interpolation with $$ and practical uses.
A raw string literal (C# 11) starts and ends with at least three double quotes and contains text exactly as written: no escape sequences, so backslashes and quotes are literal. In a multi-line raw string the opening quotes end their line, the closing quotes sit on their own line, and the whitespace before the closing quotes is removed from every line, so you can indent the literal with your code. To include """ in the text, use more quotes for the delimiters. For interpolation, the number of $ signs is the number of braces that start a hole: $$"""{ "id": {{id}} }""" keeps single braces literal, which is perfect for JSON. Add u8 for a UTF-8 ReadOnlySpan<byte>. They make JSON, SQL, regex and test fixtures readable.
Raw string literals (C# 11) let you write multi-line text containing quotes, backslashes and braces without escaping, delimited by three or more double quotes. They're ideal for JSON, SQL, XML, regex and test fixtures. I'd explain the syntax, indentation removal, quotes inside the text, interpolated raw strings with multiple $, UTF-8 raw strings, and real uses.
string single = """He said "hello" and left C:\temp"""; // single-line raw string
string json = """
{
"orderId": 42,
"status": "paid",
"path": "C:\\exports"
}
""";
""". Single-line raw strings put content on the same line; multi-line raw strings start content on the next line after the opening delimiter.\n, \\, \" are literal characters. (In the JSON above, C:\\exports stays exactly as written — which is valid JSON escaping, because the string is passed through untouched.)In a multi-line raw string, the closing delimiter's column defines the indentation to strip:
var sql = """
SELECT Id, Total
FROM Orders
WHERE CustomerId = @customerId
""";
""" (12 spaces here) is removed from every content line.So you can indent raw strings naturally with your code without the indentation leaking into the value.
If the content contains """, use more quotes for the delimiters:
var doc = """"
A raw string with """ inside it.
"""";
The delimiter must be longer than any run of quotes in the content.
$The number of $ signs sets how many braces open an interpolation hole, so single braces stay literal — perfect for JSON and C#-like templates:
var payload = $$"""
{
"orderId": {{order.Id}},
"total": {{order.Total.ToString(CultureInfo.InvariantCulture)}},
"tags": ["priority", "au"]
}
""";
$$, holes are {{expr}}; a single { or } is literal.$$$, holes are {{{expr}}}, letting {{ appear literally (e.g. Mustache/Handlebars templates).{{total:C}}).Append u8 to get a ReadOnlySpan<byte> encoded at compile time:
ReadOnlySpan<byte> body = """
{"status":"ok"}
"""u8;
await response.Body.WriteAsync(body.ToArray(), ct); // or write the span directly with PipeWriter
No runtime encoding or allocation for constant payloads. (Interpolation isn't supported with u8.)
FromSql templates (still use parameters, not interpolation of values!), migration scripts in code.[GeneratedRegex]).$$""").Cautions: don't build SQL/JSON with untrusted values via interpolation — use parameters and serialisers; raw strings make injection easier to write, not safer.
Integration tests for a payments webhook contained JSON with escaped quotes and \ns across 60 lines — nobody could read or update them, and a missing escape made one test silently send invalid JSON. Converting fixtures to raw string literals made them identical to the provider's documentation samples; we used $$""" for the few dynamic fields. Fixture-related test failures disappeared and reviewing payload changes became trivial.
The interviewer checks that you know modern string syntax and use it safely.
Green flags
$$ for JSON.StringSyntax.Red flags
For $ repeated M times:
{.Regular "..." |
Verbatim @"..." |
Raw """...""" |
|
|---|---|---|---|
| Escapes | Processed | Only "" for a quote |
None |
| Multi-line | No (use \n) |
Yes, indentation kept | Yes, indentation stripped |
| Quotes | \" |
"" |
As-is (with longer delimiters) |
| Braces in interpolation | {{ |
{{ |
Literal with $$ |
Raw strings without interpolation are compile-time constants: const string Query = """..."""; works and can be used in attributes (e.g. [GeneratedRegex("""...""")]).
The line endings inside a raw string are those of the source file (CRLF on Windows checkouts, LF elsewhere). With Git's core.autocrlf, the same code can produce different string values on different machines — a source of flaky snapshot tests. Normalise with .gitattributes (*.cs text eol=lf) or ReplaceLineEndings() when comparing.
""" inside content using four-quote delimiters.$$""" and a format specifier.using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
public static class Program
{
public static void Main()
{
int id = 42;
string name = "Mia";
// $$: single braces are literal JSON, {{ }} are holes
string json = $$"""
{
"id": {{id}},
"name": "{{name}}",
"tags": ["vip", "nsw"]
}
""";
Console.WriteLine(json);
Console.WriteLine(JsonDocument.Parse(json).RootElement.GetProperty("id").GetInt32());
// Readable SQL; values still go in as parameters, never interpolated
const string sql = """
SELECT o.Id, o.Total
FROM dbo.Orders AS o
WHERE o.CustomerId = @customerId
AND o.Status <> 'Cancelled'
ORDER BY o.CreatedUtc DESC;
""";
Console.WriteLine(sql);
// Regex without doubled backslashes
var abn = new Regex("""^\d{2}\s?\d{3}\s?\d{3}\s?\d{3}$""");
Console.WriteLine(abn.IsMatch("51 824 753 556"));
// Text containing three quotes: use four-quote delimiters
string doc = """"
Use """ to start a raw string.
"""";
Console.WriteLine(doc);
// UTF-8 bytes encoded at compile time
ReadOnlySpan<byte> body = """{"ping":true}"""u8;
Console.WriteLine(Encoding.UTF8.GetString(body));
Validate("""{"ok":true}""");
}
// IDE highlights and checks JSON passed to this parameter
static void Validate([StringSyntax(StringSyntaxAttribute.Json)] string payload)
=> Console.WriteLine(JsonDocument.Parse(payload).RootElement.GetProperty("ok"));
}
What happens: $"""SELECT ... WHERE Name = '{name}'""" passed to raw SQL APIs.
Why it is wrong: SQL injection.
Do instead: parameters (FromSql with FormattableString, Dapper parameters).
What happens: $$"""{"name":"{{name}}"}""" with user input.
Why it is wrong: quotes/backslashes in values break or inject JSON.
Do instead: use System.Text.Json for dynamic data; raw strings for fixtures.
What happens: closing """ indented more than content lines.
Why it is wrong: compile error CS9003.
Do instead: align the closing delimiter with the content's left edge.
What happens: writing \n expecting a newline.
Why it is wrong: raw strings don't process escapes.
Do instead: use actual line breaks.
$ for brace-heavy contentWhat happens: $"""{ "a": {x} }""".
Why it is wrong: literal braces are treated as holes.
Do instead: use $$ (or more).
What happens: snapshot comparisons fail across OSes. Why it is wrong: raw strings inherit source line endings. Do instead: normalise line endings.
What happens: {{total}} formats with current culture.
Why it is wrong: decimal separators differ across environments.
Do instead: format with invariant culture for machine data.
What happens: large email/HTML templates embedded in C#. Why it is wrong: hard for non-developers to edit; redeploy for text changes. Do instead: template files or a templating engine.
Raw string literals are a small feature with a large effect on test readability and review quality. When a JSON contract or SQL query is written exactly as it is sent, reviewers can compare it with documentation or a query plan without decoding escapes.
Useful team conventions:
| Practice | Why |
|---|---|
$$""" for JSON templates |
consistent, no brace doubling |
[StringSyntax] on APIs taking JSON, regex or URIs |
IDE validation and highlighting |
SQL as const raw strings plus parameters |
readable and safe |
| Snapshot tests with raw strings or verified files | clear diffs |
They also matter in source generators, where templates written as raw strings look like the generated code, making generators easier to maintain.
$"Hello {name}" is compiled, not parsed at run time. With a string target, C# 10+ uses DefaultInterpolatedStringHandler, which formats each hole straight into a pooled buffer without boxing, then allocates the final string once. Holes take alignment and format ({total,10:C}), and formatting uses the current culture, so for machine-readable text I use string.Create(CultureInfo.InvariantCulture, $"..."). Assigned to FormattableString, it keeps the format and arguments separate, which is how EF Core's FromSql($"...") turns holes into SQL parameters. Raw $$""" strings help with JSON. Two traps: interpolating into raw SQL causes SQL injection, and interpolating into log templates destroys structured logging.
In memory a .NET string is UTF-16; an Encoding converts it to and from bytes at the boundaries (files, HTTP, databases). I default to UTF-8 without a BOM (new UTF8Encoding(false)), and I use throwOnInvalidBytes: true when bad input should fail rather than silently become �. Legacy formats like Windows-1252 need CodePagesEncodingProvider registered. For speed there are "OK"u8 literals and the Utf8 class for span transcoding. For text correctness, Rune handles code points beyond the BMP, StringInfo counts grapheme clusters (what users see), and I normalise to NFC before comparing or storing user input.
Last updated 30 Oct 2026.