AlgoMaster Logo

Raw String Literals

Medium Priority20 min readUpdated June 6, 2026

Raw string literals shipped in C# 11 to solve a problem every C# developer has hit: embedding JSON, SQL, regex, or HTML inside source code without drowning in escape sequences. The _Verbatim & Raw String Literals_ lesson introduced the basics. This lesson goes deeper into the rules the compiler actually enforces, the multi-dollar interpolation syntax, the edge cases that trip people up, and the production scenarios where raw strings change how you write code.

The Triple-Quote Rule, Stated Precisely

A raw string opens with at least three double quotes and closes with the same count. Nothing inside is interpreted. No escape sequences, no quote doubling, no special meaning for braces (unless you opt into interpolation). The text between the fences is the string.

The "at least three" part matters. The minimum is three, but the actual rule is that the fence must be longer than any run of consecutive " characters inside the content. Three quotes is enough when the content has zero, one, or two quotes in a row. Four quotes is required when the content contains """. Five is required when the content contains """", and so on.

Read those carefully. The first string contains two consecutive quotes (""), so the three-quote fence is enough. The second contains three consecutive quotes ("""), so the fence widens to four. The third contains four consecutive quotes inside text, so the fence widens to five. The pattern keeps scaling.

Raw strings have no escape mechanism at all. If you need quotes in the content, widen the fence. You never end up doubling characters or sprinkling backslashes through your data.

The Single-Line Form vs Multi-Line Form

Raw strings come in two distinct shapes, and the compiler treats them differently.

The single-line form keeps everything on one source line. The opening fence, the content, and the closing fence all sit on the same line. No leading or trailing newline is part of the string:

The string is exactly 21 characters. No hidden newline at the start or end.

The multi-line form is structurally different. The opening """ must be the last token on its line, the closing """ must be the first non-whitespace on its line, and the content lives on the lines between them. The newline immediately after the opening fence and the newline immediately before the closing fence are not part of the string. They are structural markers, not content.

Same 21-character string. The blank line after """ and before the closing """ are excluded automatically. If the content were two lines long, the resulting string would have one \n between them but no leading or trailing newline.

This rule removes a whole class of bugs. You never have to remember whether your multi-line string includes a leading newline. It never does, unless you explicitly add a blank content line.

The Indentation Stripping Algorithm

This is the part that surprises people most often. When you write a multi-line raw string, the compiler does not preserve the indentation you typed. It strips a prefix of whitespace from every content line, and the closing """ determines how much.

The algorithm is precise. The closing """ sits at some column, call it column N. Every content line must start with at least N characters of whitespace, and the compiler removes exactly that many characters from the start of each line. Whatever remains is the actual string content.

The closing """ is indented four spaces. So four spaces get stripped from every content line. The first content line had eight leading spaces, so four remain. The second had twelve, so eight remain. The third had eight, so four remain. The relative indentation is preserved, but the absolute indentation matches whatever the closing fence dictates.

The diagram shows the same idea visually. The closing delimiter column acts as the left margin. The compiler's job is to take a normalized chunk of text and remove that exact prefix from each line.

Two rules apply. All whitespace prefixes must use the same characters. The compiler matches exactly, so if the closing fence is indented with four spaces but a content line is indented with a tab, the compiler reports CS9003. Mixing tabs and spaces in the indentation is rejected. Every content line must be indented at least as far as the closing fence, with one exception: a completely blank line in the content is allowed regardless of its leading whitespace.

The blank line in the middle has no characters at all, not even spaces. That's fine. The compiler treats it as a blank line in the output without complaining about insufficient indentation.

Embedding JSON Without Escapes

The single biggest payoff of raw strings is JSON. A typical product JSON written as a regular string is unreadable:

The same content as a raw string reads exactly like JSON:

Every quote is just a quote. The two-space indentation inside the JSON object is preserved because those lines are indented two more spaces than the closing fence. No escape sequences anywhere.

This pattern is especially useful for test fixtures. When you're writing a test that needs a known-good JSON payload to deserialize or compare against, a raw string keeps the test readable.

The JSON loads with JsonDocument.Parse exactly as it would from a file. Raw strings have no runtime overhead. The compiler produces the same kind of string object that any other literal produces, and the indentation work happens once at compile time.

Embedding SQL Queries

SQL queries with multiple clauses are another natural fit. A query that touches several columns and has a multi-line WHERE clause is much easier to read as a raw string than as concatenated single-line literals.

The four-space indent on column names and the two-space indent on AND lines are both preserved relative to the closing fence. Reviewers can read the SQL the way they would in any database client.

A raw string literal compiles to the same string object you'd get from any other literal. The indentation stripping happens once during compilation, not on every reference. Choose between raw, verbatim, and regular based on readability, not performance.

For parameterized queries in production, you'd typically pair raw strings with a query builder or ORM rather than concatenating values directly. Raw strings make the query template readable, but they don't replace parameterization for safety.

Regex Patterns Without Backslash Doubling

Regex patterns are the textbook case for raw strings. A pattern like \d{3}-\d{4} looks reasonable in a verbatim string (@"\d{3}-\d{4}"), but more complex patterns with character classes, lookarounds, and quote characters get noisy. Raw strings let you paste a pattern from a regex tester and have it work.

The pattern contains backslashes (\., \.) that stay backslashes. It contains hyphens, dots, plus signs, and brackets, all preserved verbatim. The single-line form is fine here because the pattern fits on one line. There are no quote characters in the content, so three quotes are enough on each side.

A more complex case: validating a coupon code that follows the pattern SAVE-XXXX-YYYY where XXXX is alphanumeric and YYYY is digits.

The pattern reads cleanly. No backslash doubling, no escape sequences. For multi-line regex with the IgnorePatternWhitespace option (also known as "verbose mode" or "free-spacing mode"), raw strings are even more useful, because you can lay out the pattern across several lines with explanatory comments.

Interpolation with $"""..."""

Single-dollar interpolation in a raw string works the same way as in a regular interpolated string. Braces mark interpolation holes, and the expression inside the braces is evaluated and converted to a string.

The :C format specifier formats the decimal as currency. Everything else inside braces is a plain expression. This works fine for any content that doesn't contain literal { or } characters.

A subtle but useful feature: interpolation expressions in a raw string can span multiple source lines. The compiler treats the contents of an interpolation hole as a C# expression, so newlines and additional whitespace inside the braces don't affect the resulting string. This is handy for longer expressions.

The prices.Sum() call is a single expression. It happens to fit on one line, but it could span multiple if the expression were more complex, and the raw string would still produce the same output.

Multi-Dollar Interpolation: $$"""..."""

This is where raw strings pull ahead of every other string form. When your content needs literal { or } characters, doubling them as {{ and }} works but quickly becomes painful. JSON objects, HTML attribute templates, and CSS code blocks are full of braces.

The fix: add a second $. With $$"""...""", a single brace is a literal character, and only {{ and }} mark interpolation. The rule is general: the number of $ signs equals the number of consecutive braces required to mark interpolation. Three $ signs means {{{ opens an interpolation hole, and so on.

The JSON object braces ({ and }) are single characters in the source and single characters in the output. The interpolation holes use {{orderId}} because two braces in a row match the $$ count. Compare this with the single-dollar form, where you'd have to write {{ for every literal {, doubling the brace count throughout the JSON.

For HTML templates that include JavaScript or CSS in attributes or script blocks, three dollars can be useful:

The CSS block contains single { and } characters around the style rules. With $$$, those single braces stay literal. The interpolation holes use {{{orderId}}} and {{{customer}}}, which is admittedly less common, but the trade-off is clean CSS without doubled braces all over the style block.

Most code only ever needs one or two dollar signs. Three becomes useful when generating code in languages whose syntax already involves doubled braces, like Mustache templates or some JSX patterns.

Raw Strings vs Verbatim: When Each Wins

The strings section already compared the three forms in detail. The short version: raw strings beat verbatim whenever the content contains quote characters or whenever the content benefits from indentation-aware formatting. Verbatim still wins for short single-line strings with backslashes, like file paths and simple regex patterns.

ScenarioBest ChoiceWhy
Windows file pathVerbatim (@"...")Short, single-line, full of backslashes
Simple regex (\d+, \w+)VerbatimCompact, no quotes in pattern
Complex regex with quotesRawNo quote doubling required
JSON literalRaw with $$Literal braces stay literal
Multi-clause SQLRawIndentation handling keeps the query readable
HTML templateRaw with $$ or $$$Quotes and braces both appear in content
Short interpolated messageRegular ($"...")Raw is overkill for one-line strings
Path with interpolationVerbatim interpolated ($@"...")Single line, backslashes plus a placeholder

The decision is usually obvious once you look at the content. If it has quotes, raw. If it has braces, raw with multi-dollar. If it's a one-liner with backslashes, verbatim. If it's a short message with placeholders, regular interpolation.

Common Pitfalls

A few mistakes show up often when developers first use raw strings.

Mistake 1: Indenting the closing fence less than the content.

This compiles. The closing fence is at column 4, the content is at column 8. The compiler strips 4 spaces from each content line, leaving " Hello\n World". The four-space leading indent on each output line surprises people who expected flush-left output. If you wanted no leading indent, the closing fence should sit at column 8.

Mistake 2: Indenting the closing fence more than the content.

CS9003. Every content line must be indented at least as far as the closing fence. The compiler refuses to produce a string with negative indentation. The fix is to indent the content further, or move the closing fence to match.

Mistake 3: Putting content on the opening line.

CS9101. The multi-line form requires the opening """ to be the last thing on its line. If you want content right after the opening fence, you have to use the single-line form, which means everything has to fit on one line.

Mistake 4: Mixing tabs and spaces in indentation.

If your closing fence is indented with four spaces but a content line starts with a tab (even one that visually aligns), the compiler reports an error. The whitespace prefix has to match character-for-character. Most teams set their editor to insert spaces only, which sidesteps this entirely.

Mistake 5: Forgetting that the leading and trailing newlines are excluded.

The string is just "Hello", five characters. No leading or trailing newline. If you actually want a trailing newline in the string, add a blank content line before the closing fence.

Generated Code: A Compile-Time Tool

Raw strings have changed how source generators are written. Before C# 11, writing a source generator that emits another class or method meant either concatenating string fragments or using verbatim strings with "" for every embedded quote. The result was unreadable code that generated unreadable code.

With raw strings and multi-dollar interpolation, a source generator can produce real, readable C# code from a template:

The template is readable, the placeholders are obvious, and the output is real C# code that the compiler can consume. This is exactly the pattern that source generators in Microsoft's own libraries (System.Text.Json, the LoggerMessage generator, the RegexGenerator) use internally.

You don't need to be writing a source generator to benefit from this. The same pattern works for any code that builds text from a template: email bodies, configuration files, scripted shell commands, documentation snippets.

Quiz

Raw String Literals Quiz

10 quizzes