What's New in C# 11: Raw Strings, required and List Patterns
How we wrote it in C# 10 and how we write it in C# 11: raw string literals and the culture trap, required members and SetsRequiredMembers, list patterns, generic math with its language and library legs, file-local types and UTF-8 literals.
Version note: This post covers the language features that came with C# 11; the code was compiled against C# 11 / .NET 7 with the .NET 10 SDK and run.
Guess First
Let's start with C# 11's most loved addition: the raw string literal, opened with three quotes. The code below prints a book as JSON. I set the culture to tr-TR on purpose, because that may well be the default on a server in Turkey. Is the output valid JSON?
using System.Globalization;
CultureInfo.CurrentCulture = new CultureInfo("tr-TR");
string title = "Clean Code";
decimal price = 42.5m;
string json = $$"""
{
"title": "{{title}}",
"price": {{price}}
}
""";
Console.WriteLine(json);
Raw String Literals
It isn't. The output is:
{
"title": "Clean Code",
"price": 42,5
}
42,5 is not a number in JSON; a JSON parser fails on this line. The raw string isn't to blame, the interpolation is: {{price}} is turned into a string using the current culture, and in Turkish the decimal separator is a comma. We'll come back to this trap below. First, let's see what the raw string solves. In C# 10 we wrote the same JSON like this:
string json = $@"{{
""title"": ""{title}"",
""price"": {price},
""tags"": [""craft"", ""c#""]
}}";
In a verbatim interpolated string you write every quote twice and every curly brace twice. Even a short JSON becomes hard to read, and pasting a long payload into a test file turns into a copy-and-fix job. The C# 11 version:
string json = $$"""
{
"title": "{{title}}",
"price": {{price}},
"tags": ["craft", "c#"]
}
""";
{
"title": "Clean Code",
"price": 42.5,
"tags": ["craft", "c#"]
}
This time I set the culture to CultureInfo.InvariantCulture and got 42.5. A raw string opens with at least three quotes and closes with the same number; quotes inside it need no escaping. The column of the closing quotes matters: the whitespace to its left is removed from every line. The JSON I indented by four spaces in the code starts at column zero in the output. The leading $$ means "I'll use two curly braces for interpolation". That way the JSON's own single braces stay as they are, while {{title}} writes the variable's value. Compiling a small test file with a raw string under LangVersion 10 gives:
Program.cs(2,15): error CS8936: Feature 'raw string literals' is not available in C# 10.0. Please use language version 11.0 or greater.
required Members
In C# 10 the only solid way to stop a Book from being created without an ISBN was a constructor:
public class Book
{
public Book(string isbn, string title, decimal price)
{
Isbn = isbn;
Title = title;
Price = price;
}
public string Isbn { get; }
public string Title { get; }
public decimal Price { get; }
}
It works, but you lose the readability of an object initializer, and the constructor's parameter list grows with every property. C# 11 brought the required modifier instead:
public class Book
{
public required string Isbn { get; init; }
public required string Title { get; init; }
public decimal Price { get; init; }
public override string ToString() => $"{Title} ({Isbn}) {Price}";
}
new Book { Isbn = "978-0132350884", Title = title, Price = price } compiles and prints Clean Code (978-0132350884) 42.5. Forget Isbn and write var book = new Book { Title = "Clean Code" };, and the compiler stops you:
Program.cs(1,16): error CS9035: Required member 'Book.Isbn' must be set in the object initializer or attribute constructor.
Price isn't required; if you don't set it, it stays 0. The rule works at compile time. Compile with C# 10 and you get the familiar pattern: Feature 'required members' is not available in C# 10.0. Please use language version 11.0 or greater.
List Patterns
Think of a function that describes the line quantities of an order in one line. In C# 10 you wrote it with length checks and indexes:
static string Describe(int[] quantities) =>
quantities.Length == 0 ? "empty order"
: quantities.Length == 1 ? $"one line, qty {quantities[0]}"
: $"{quantities.Length} lines, first {quantities[0]}, last {quantities[^1]}";
With C# 11 you can write the shape of the array directly as a pattern:
static string Describe(int[] quantities) => quantities switch
{
[] => "empty order",
[var single] => $"one line, qty {single}",
[var first, .., var last] => $"{quantities.Length} lines, first {first}, last {last}"
};
empty order
one line, qty 2
4 lines, first 2, last 5
[] matches an empty array and [var single] one with a single element. The .. slice pattern means "however many elements in between"; it lets you name the first and last elements at the same time. The real gain is safety more than readability: since you don't touch indexes by hand, you can't make the mistake of reading quantities[0] on an empty array and getting an exception. The C# 10 error: Feature 'list pattern' is not available in C# 10.0. Please use language version 11.0 or greater.
Generic Math: Language and Library Together
In C# 10 you couldn't write "a single generic method that sums any numeric type". There was no constraint that let you ask for a + operator or a zero value for T; that's also why LINQ's Sum comes with separate overloads for int, long, decimal and double. With C# 11 and .NET 7:
using System.Numerics;
public static class Pricing
{
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
T total = T.Zero;
foreach (T value in values)
total += value;
return total;
}
}
6
80.5
0.30000000000000004
The same method worked with int, decimal and double arrays; the floating-point result on the last line is just the familiar behavior of double. T.Zero and += became possible because INumber<T> and the interfaces it inherits define them as static abstract members. This feature stands on two legs, and I saw both by breaking them one at a time. Targeting net7.0 with LangVersion 10, the language leg is missing:
Program.cs(7,19): error CS8936: Feature 'static abstract members in interfaces' is not available in C# 10.0. Please use language version 11.0 or greater.
Program.cs(9,13): error CS8936: Feature 'static abstract members in interfaces' is not available in C# 10.0. Please use language version 11.0 or greater.
Targeting net6.0, even with LangVersion set to 11, the library leg is missing, because INumber<T> isn't in the .NET 6 BCL:
Program.cs(5,61): error CS0246: The type or namespace name 'INumber<>' could not be found (are you missing a using directive or an assembly reference?)
We look at how to use generic math in your own generic collections in detail in the tenth post of the C# Collections and Generics series.
File-Local Types and UTF-8 Literals
For a small helper class that should stay inside one file, the narrowest access you had in C# 10 was internal, which means the whole assembly could see it. C# 11's file modifier locks a type inside a single file:
using System.Globalization;
public static class Invoice
{
public static string Line(string title, decimal price) => $"{title}: {PriceFormatter.Format(price)}";
}
file static class PriceFormatter
{
public static string Format(decimal price) => price.ToString("0.00", CultureInfo.InvariantCulture) + " TRY";
}
In the same project the Label.cs file defines its own file static class PriceFormatter and the two don't clash. In Program.cs, calling Invoice.Line("Clean Code", 42.5m) printed Clean Code: 42.50 TRY; but when I tried to call PriceFormatter.Format directly, I got error CS0103: The name 'PriceFormatter' does not exist in the current context.
The UTF-8 string literal is handy in HTTP and protocol code. In C# 10 you reached the bytes with Encoding.UTF8.GetBytes("application/json"), which means a new array on every call. In C# 11, with the u8 suffix, the literal becomes a ReadOnlySpan<byte> directly:
ReadOnlySpan<byte> contentType = "application/json"u8;
Console.WriteLine($"{contentType.Length} bytes, first: {contentType[0]}");
16 bytes, first: 97
97 is the UTF-8 code of a lowercase a. The C# 10 error: Feature 'UTF-8 string literals' is not available in C# 10.0. Please use language version 11.0 or greater.
Smaller but Useful Ones
C# 11's list doesn't end there. Generic attributes are no longer a preview: you can write class MyAttr<T> : Attribute and pass a type argument instead of a typeof parameter. Expressions inside an interpolation can span multiple lines. In structs, fields the constructor doesn't assign get their default value automatically (auto-default structs). nameof can see parameter names in attributes on a method. The >>> unsigned right shift operator and checked user-defined operators arrived. nint and nuint became simple types that alias IntPtr and UIntPtr. On the performance side there are ref fields and scoped ref. Finally, warning wave 7 arrived; we'll see its CS8981 warning for lowercase type names in a real project in the post about upgrading versions.
Common Pitfalls
Treating raw strings as a JSON tool. As you saw in the opening question, a raw string only makes the text easier to write. Culture-dependent formatting and escaping remain your job. If the book title contained a quote, the JSON would break again. Raw strings are great for test data or log messages; for a real payload you send to an API, use JsonSerializer.
Treating required as validation. The compiler only checks whether the property is assigned in the initializer, not what the value means. In the example you'll see shortly, new Book { Isbn = "", Title = "Refactoring" } compiled fine and Isbn stayed an empty string.
Assuming a constructor makes required go away. Even if you write a constructor that sets every property, the compiler doesn't know that. Calling new Book("978-0132350884", "Clean Code") with an unmarked constructor gave these two errors:
Program.cs(1,13): error CS9035: Required member 'Book.Isbn' must be set in the object initializer or attribute constructor.
Program.cs(1,13): error CS9035: Required member 'Book.Title' must be set in the object initializer or attribute constructor.
The fix is to mark the constructor with [SetsRequiredMembers]:
using System.Diagnostics.CodeAnalysis;
var a = new Book("978-0132350884", "Clean Code");
var b = new Book { Isbn = "", Title = "Refactoring" };
Console.WriteLine($"[{a.Title}] [{b.Isbn}]");
public class Book
{
public Book() { }
[SetsRequiredMembers]
public Book(string isbn, string title)
{
Isbn = isbn;
Title = title;
}
public required string Isbn { get; init; }
public required string Title { get; init; }
}
[Clean Code] []
This attribute is a promise: you're saying "this constructor really sets all of them", and the compiler trusts you. If you add a new required property later, don't forget to keep that promise.
Trying to use generic math in a .NET 6 project. As you saw above, raising LangVersion isn't enough; you need to target .NET 7 or later for INumber<T>.
A Small Experiment
★☆☆ Produce the JSON from the opening question with JsonSerializer.Serialize(new { title, price }) and check whether the output changes under the tr-TR culture.
★★☆ Add a separate arm to Describe for orders whose first line is greater than 10: [> 10, ..]. How does the order of the arms affect the result?
★★★ Add an Average<T> to Pricing that uses INumber<T>. What should it do for an empty collection, and how does division behave for int?
In the next post we move on to C# 12: primary constructors, square-bracket collection expressions, default parameters in lambdas, and giving any type an alias with using. We'll also see there why a primary constructor behaves differently from the one in records.