← Back to Blog

C# 11/12 New Features: Records, Required Members, Raw String Literals

When writing modern C# applications, you often end up juggling multiple concerns at once-immutability, correct initialization, readable configuration, and reducing boilerplate. Versions 11 and 12 of C# bring features that help address these concerns elegantly: records for immutable data, required members to enforce proper initialization, and raw string literals to make multi-line, structured text readable. These features might seem small individually, but together they enable patterns that can make your code more robust and maintainable.

Let’s take a deep dive into each of these features, but rather than just show syntax, we’ll explore the underlying concepts, practical scenarios, and subtle nuances that affect how you design software.

Records: The Backbone of Immutable Data

In C#, a class is mutable by default, meaning anyone can change its properties after creation. That’s fine in some contexts, but when you’re dealing with data models that represent configuration, API responses, or state snapshots, immutability becomes crucial. Records provide a clean, efficient way to define immutable types.

At first glance, records look like syntactic sugar-they automatically implement value-based equality, a readable ToString(), and support deconstruction. But their real value comes from enforcing patterns that minimize accidental state changes.

public record Person(string Name, int Age);

var alice = new Person("Alice", 30);
var copyAlice = alice with { Age = 31 };

Console.WriteLine(alice);       // Person { Name = Alice, Age = 30 }
Console.WriteLine(copyAlice);   // Person { Name = Alice, Age = 31 }

Notice the with expression. It allows you to create a modified copy of a record without mutating the original. This is extremely useful in scenarios like event sourcing, state management in applications, or any case where you need snapshots of state at a particular moment.

With C# 11/12, you can also enforce validation logic on init-only properties in records. This ensures that even immutable objects are always valid when created:

public record Person
{
    private string _name = "";

    public string Name
    {
        get => _name;
        init
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Name cannot be empty");
            _name = value;
        }
    }

    public int Age { get; init; }
}

This pattern is subtle but important. By combining immutability with validation, you prevent both accidental state changes and invalid objects. In larger codebases, this reduces the chances of bugs propagating silently.

Required Members: Compiler-Enforced Correctness

A common bug in object-oriented programming is forgetting to set required fields. Even with immutable objects, developers might omit essential properties, leading to runtime exceptions. Required members solve this by making the compiler enforce that certain properties must be initialized.

public class User
{
    public required string Username { get; init; }
    public required string Email { get; init; }
    public string? PhoneNumber { get; init; }
}

// This compiles:
var user = new User
{
    Username = "john_doe",
    Email = "[email protected]"
};

// This would fail at compile time:
// var invalidUser = new User { Username = "jane" }; // Missing Email

Required members are particularly powerful in domains where certain data must always be present-think financial transactions, API request models, or user profiles. The compiler acts as a safety net, catching mistakes before your code ever runs. Combined with records, required members allow you to define data models that are both immutable and guaranteed to be valid.

Raw String Literals: Human-Readable Multi-Line Text

Handling JSON, SQL, or any multi-line text in C# used to be a headache. Escaping quotes, backslashes, and preserving formatting often led to hard-to-read code. Raw string literals eliminate this pain by letting you write strings exactly as they appear in your editor.

string json = """
{
    "name": "John",
    "age": 30
}
""";

// Interpolation works seamlessly
string name = "John";
int age = 30;
string interpolated = $"""
{{
    "name": "{name}",
    "age": {age}
}}
""";

Raw strings maintain indentation, spacing, and readability. When dealing with configuration files, embedded SQL queries, or templates, this can make code much easier to maintain. Additionally, combining raw strings with interpolation lets you embed dynamic values cleanly, without cluttering the syntax with escape sequences.

Putting It Together: A Real-World Example

Imagine building a configuration system for a microservice. You need immutable objects to represent configuration snapshots, required members to ensure correctness, and readable embedded JSON for defaults. Here’s how these features come together:

public record DatabaseConfig
{
    public required string ConnectionString { get; init; }
    public required string Provider { get; init; }
    public int MaxPoolSize { get; init; } = 100;
}

public record AppConfig
{
    public required string AppName { get; init; }
    public required DatabaseConfig Database { get; init; }
    public string? LogLevel { get; init; }
}

// Using raw string literal for JSON config
var configJson = """
{
    "AppName": "MyService",
    "Database": {
        "ConnectionString": "Server=localhost;Database=mydb",
        "Provider": "SqlServer"
    },
    "LogLevel": "Information"
}
""";

This approach enforces correctness through required members, preserves readability with raw strings, and ensures immutability with records. If the configuration changes, you can create a new AppConfig object without mutating the original, making it easier to reason about application state.

Subtle Nuances for Experienced Developers

One thing to keep in mind with records is that equality checks are value-based by default. If your record contains mutable collections, equality might behave unexpectedly. It’s best to combine records with immutable collections or ensure that the properties themselves don’t change. Similarly, required members only enforce initialization; they don’t enforce runtime constraints. You can still include validation logic in init accessors for extra safety.

Raw string literals are great for readability, but be mindful of indentation. The compiler preserves all whitespace relative to the least-indented line. This makes it easier to write human-readable text but requires consistency in your formatting.

Best Practices in Large Applications

Records should be used for data-centric objects rather than classes that encapsulate complex behavior. Required members enforce domain rules and should be used for all essential fields. Raw strings simplify configuration and embedded templates. Together, these features promote patterns that reduce boilerplate, enforce correctness, and make your code self-documenting.

In microservices, immutable records can represent API responses. Required members ensure every critical field is present. Raw string literals make embedding JSON or SQL in code clean and maintainable. Combining these patterns leads to safer, more readable, and maintainable systems.

Summary

C# 11 and 12 bring features that go beyond syntax-they help shape better development practices. Records simplify immutable data management, required members enforce compile-time correctness, and raw string literals improve readability of multi-line text. Used together thoughtfully, these features allow you to write code that’s easier to reason about, harder to break, and much more maintainable in large projects.

Understanding not just how to use these features but also when and why they matter is key. Immutability, validation, and readability are fundamental concerns in professional software development, and these C# features tackle them head-on.