C# 11-12 Features
Master raw strings, required members, and collection expressions
C# 11-12 Features
Modern C# development requires mastery of the latest language features that enhance code clarity and performance. This lesson covers raw string literals, required members, list patterns, generic attributes, and file-scoped typesβessential capabilities introduced in C# 11 and 12 that every professional developer should understand. Practice these concepts with free flashcards and spaced repetition to solidify your expertise.
Welcome to Modern C# π»
C# 11 and 12 represent significant evolutionary steps in the language's maturity, focusing on developer productivity, code safety, and pattern matching power. Released alongside .NET 7 and .NET 8 respectively, these versions introduce features that reduce boilerplate code, enhance string handling, strengthen type safety, and provide more expressive syntax for common scenarios.
Whether you're maintaining legacy systems or building greenfield applications, understanding these features will help you write cleaner, more maintainable code that leverages the full power of the modern .NET ecosystem.
Core Concepts: C# 11 Features π§
Raw String Literals
Raw string literals eliminate the need for escape sequences when working with multi-line strings, JSON, XML, or any content containing quotes. They use triple quotes (""") as delimiters and preserve all whitespace and special characters exactly as written.
Key characteristics:
- Begin and end with at least three double quotes (
""") - Can span multiple lines
- No escape sequences needed (no
\n,\",\\) - Whitespace is preserved based on the closing delimiter's indentation
- Can include string interpolation with
$""" ... """
| Traditional String | Raw String Literal |
|---|---|
"Line 1\nLine 2" | """ |
"{\"key\": \"value\"}" | """ |
π‘ Tip: The indentation of the closing """ determines which whitespace is trimmed from all lines. Everything to the left of the closing quotes is removed from each line.
Required Members
The required modifier ensures that specific properties or fields must be initialized during object construction, preventing null-related bugs and enforcing initialization contracts at compile time.
Benefits:
- Compile-time validation of object initialization
- Works with object initializers and constructors
- Clearer API contracts
- Reduces runtime null reference exceptions
public class Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public string? MiddleName { get; init; } // Optional
}
// β
Valid - all required members set
var person = new Person
{
FirstName = "John",
LastName = "Smith"
};
// β Compile error - missing required members
var invalid = new Person { FirstName = "John" };
β οΈ Important: The required modifier works with init accessors and constructors. If a constructor sets required members, callers don't need to use initializers.
List Patterns
List patterns extend pattern matching to collections, allowing you to match against array and list structures with elegant, declarative syntax.
Pattern types:
- Discard patterns:
[_, _, _]matches any three-element collection - Slice patterns:
[1, .., 5]matches collections starting with 1 and ending with 5 - Type patterns:
[int x, string s]matches and captures typed elements - Constant patterns:
[0, 1, 2]matches exact values
public static string AnalyzeArray(int[] numbers) => numbers switch
{
[] => "Empty",
[var single] => $"Single element: {single}",
[var first, var second] => $"Pair: {first}, {second}",
[var first, .., var last] => $"First: {first}, Last: {last}",
_ => "Other"
};
Generic Attributes
C# 11 allows generic attributes, enabling type-safe attribute definitions without requiring typeof() parameters or runtime type checking.
Before C# 11:
public class TypedAttribute : Attribute
{
public Type Type { get; }
public TypedAttribute(Type type) => Type = type;
}
[Typed(typeof(string))] // Runtime type passing
public class MyClass { }
With C# 11:
public class TypedAttribute<T> : Attribute
{
public Type Type => typeof(T);
}
[Typed<string>] // Compile-time type safety
public class MyClass { }
π‘ Benefit: Eliminates magic strings and runtime type errors, providing IntelliSense support and compile-time validation.
File-Scoped Types
The file modifier restricts type visibility to the file where it's declared, perfect for implementation details that shouldn't leak across your codebase.
file class InternalHelper
{
public static string Process(string input) => input.ToUpper();
}
file interface IInternalContract
{
void Execute();
}
public class PublicService
{
public string Transform(string value)
{
// Can use file-scoped types within same file
return InternalHelper.Process(value);
}
}
Use cases:
- Source generator implementation details
- Private helper classes
- Internal contracts not meant for reuse
- Reducing namespace pollution
Core Concepts: C# 12 Features π
Primary Constructors
Primary constructors allow you to declare constructor parameters directly in the class declaration, eliminating boilerplate field declarations and assignments.
Syntax:
public class Customer(string name, int id)
{
// Parameters available throughout the class
public string Name => name;
public int Id => id;
public void Display()
{
Console.WriteLine($"{name} (ID: {id})");
}
}
π§ Memory device: Think of primary constructors as "parameter-level fields"βthey're accessible everywhere in the class but don't create explicit fields unless you use them in ways that require capture.
Comparison with traditional approach:
| Traditional Constructor | Primary Constructor |
|---|---|
public class Customer
{
private readonly string _name;
private readonly int _id;
public Customer(string name, int id)
{
_name = name;
_id = id;
}
} |
public class Customer(string name, int id)
{
// Parameters directly accessible
// No explicit field declaration
} |
β οΈ Important: Primary constructor parameters are captured as needed. If you only use them in initialization, no backing field is created. If you use them in methods or properties, they're captured.
Collection Expressions
Collection expressions provide unified syntax for creating arrays, lists, spans, and other collection types using square brackets [].
Syntax patterns:
// Arrays
int[] numbers = [1, 2, 3, 4, 5];
// Lists
List<string> names = ["Alice", "Bob", "Charlie"];
// Spans
Span<int> span = [10, 20, 30];
// Spread operator for combining
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] combined = [..first, ..second]; // [1, 2, 3, 4, 5, 6]
// Empty collections
List<int> empty = [];
π‘ Benefit: Consistent syntax across all collection types reduces cognitive load and makes code more readable.
Alias Any Type
C# 12 extends the using directive to alias any type, including tuples, pointers, and nullable typesβnot just named types.
Examples:
// Tuple aliases
using Point = (int X, int Y);
using Dimensions = (int Width, int Height, int Depth);
// Nullable aliases
using OptionalString = string?;
// Array aliases
using Matrix = int[][];
// Complex generic aliases
using ResultDictionary = Dictionary<string, Result<int, string>>;
public class Example
{
public Point Origin = (0, 0);
public OptionalString? Name = null;
}
π§ Use case: Creating semantic names for complex types improves code readability without the overhead of creating new type wrappers.
Inline Arrays
Inline arrays enable fixed-size buffer allocations directly in structs, providing performance benefits for low-level scenarios without unsafe code.
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10
{
private int _element0;
}
public class Example
{
public void UseBuffer()
{
Buffer10 buffer = default;
buffer[0] = 100;
buffer[9] = 900;
// Works with spans
Span<int> span = buffer;
}
}
β οΈ Note: This is an advanced feature primarily for high-performance scenarios, interop, or library authors. Most application code won't need inline arrays.
Experimental Attribute
The ExperimentalAttribute formally marks APIs as experimental, generating compiler warnings when used and requiring explicit opt-in.
using System.Diagnostics.CodeAnalysis;
[Experimental("MYLIB001")]
public class ExperimentalFeature
{
public void NewMethod() { }
}
// Using experimental API generates warning
public class Consumer
{
public void Test()
{
var feature = new ExperimentalFeature(); // Warning MYLIB001
}
}
// Suppress warning with pragma
#pragma warning disable MYLIB001
var experimental = new ExperimentalFeature();
#pragma warning restore MYLIB001
Practical Examples π οΈ
Example 1: Raw String Literals for JSON
Scenario: You're building an API testing tool that needs to generate JSON payloads without escape sequence headaches.
public class ApiTester
{
public string GenerateUserPayload(string name, string email, int age)
{
// Old way - error-prone with escapes
// var json = "{\"user\": {\"name\": \"" + name + "\"}}";
// New way - clean and readable
return $"""
{
"user": {
"name": "{name}",
"email": "{email}",
"age": {age},
"metadata": {
"created": "{DateTime.UtcNow:O}",
"source": "api_test"
}
}
}
""";
}
public string GenerateRegexPattern()
{
// No need to escape backslashes!
return """
^\d{3}-\d{2}-\d{4}$
""";
}
}
Why this works: Raw string literals preserve all characters including quotes and backslashes, making JSON, SQL, regex, and XML strings dramatically more readable.
Example 2: Required Members for DTOs
Scenario: You're building a REST API and want to ensure DTOs are always properly initialized, catching errors at compile time rather than runtime.
public class CreateOrderRequest
{
public required string CustomerId { get; init; }
public required List<OrderItem> Items { get; init; }
public required decimal TotalAmount { get; init; }
// Optional fields don't use required
public string? PromoCode { get; init; }
public string? Notes { get; init; }
}
public class OrderItem
{
public required string ProductId { get; init; }
public required int Quantity { get; init; }
public required decimal Price { get; init; }
}
public class OrderService
{
public void ProcessOrder(CreateOrderRequest request)
{
// Guaranteed at compile time:
// - CustomerId is not null
// - Items is not null
// - TotalAmount is set
Console.WriteLine($"Processing order for {request.CustomerId}");
Console.WriteLine($"Items: {request.Items.Count}");
}
public void Example()
{
// β
Compiles - all required members set
var validRequest = new CreateOrderRequest
{
CustomerId = "CUST123",
Items = [new OrderItem
{
ProductId = "PROD456",
Quantity = 2,
Price = 29.99m
}],
TotalAmount = 59.98m
};
// β Won't compile - missing TotalAmount
// var invalid = new CreateOrderRequest
// {
// CustomerId = "CUST123",
// Items = []
// };
}
}
Impact: This prevents an entire class of null reference exceptions by shifting validation from runtime to compile time.
Example 3: List Patterns for Command Parsing
Scenario: You're building a command-line tool that needs to parse and validate command arguments elegantly.
public class CommandParser
{
public string ParseCommand(string[] args) => args switch
{
// No arguments
[] => "Usage: app <command> [options]",
// Single command
["help"] => ShowHelp(),
["version"] => ShowVersion(),
// Command with single argument
["user", var userId] => GetUser(userId),
["delete", var id] => DeleteItem(id),
// Command with multiple arguments
["create", var name, var email] => CreateUser(name, email),
// Variable length with first/last
["process", var first, .., var last] =>
$"Processing from {first} to {last} with {args.Length - 2} items",
// Specific patterns
["config", "set", var key, var value] => SetConfig(key, value),
["config", "get", var key] => GetConfig(key),
// List with specific length
["batch", .. var items] when items.Length > 0 =>
ProcessBatch(items),
// Default
_ => $"Unknown command: {string.Join(' ', args)}"
};
private string ShowHelp() => "Help text...";
private string ShowVersion() => "Version 1.0.0";
private string GetUser(string id) => $"Getting user {id}";
private string DeleteItem(string id) => $"Deleting item {id}";
private string CreateUser(string name, string email) =>
$"Creating user {name} ({email})";
private string SetConfig(string key, string value) =>
$"Set {key} = {value}";
private string GetConfig(string key) => $"Config value for {key}";
private string ProcessBatch(string[] items) =>
$"Processing {items.Length} items in batch";
}
// Usage examples
var parser = new CommandParser();
Console.WriteLine(parser.ParseCommand([]));
// Output: "Usage: app <command> [options]"
Console.WriteLine(parser.ParseCommand(["user", "12345"]));
// Output: "Getting user 12345"
Console.WriteLine(parser.ParseCommand(["create", "Alice", "alice@example.com"]));
// Output: "Creating user Alice (alice@example.com)"
Console.WriteLine(parser.ParseCommand(["process", "A", "B", "C", "D", "E"]));
// Output: "Processing from A to E with 3 items"
Advantages: List patterns eliminate complex index-checking logic and null guards, making command parsing code declarative and maintainable.
Example 4: Primary Constructors with Dependency Injection
Scenario: You're building services with dependency injection and want to reduce boilerplate while maintaining clarity.
// Traditional approach (before C# 12)
public class TraditionalOrderService
{
private readonly ILogger<TraditionalOrderService> _logger;
private readonly IRepository<Order> _repository;
private readonly IEmailService _emailService;
public TraditionalOrderService(
ILogger<TraditionalOrderService> logger,
IRepository<Order> repository,
IEmailService emailService)
{
_logger = logger;
_repository = repository;
_emailService = emailService;
}
public async Task ProcessOrderAsync(int orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
var order = await _repository.GetByIdAsync(orderId);
await _emailService.SendConfirmationAsync(order.CustomerEmail);
}
}
// Modern approach with primary constructor (C# 12)
public class ModernOrderService(
ILogger<ModernOrderService> logger,
IRepository<Order> repository,
IEmailService emailService)
{
// No field declarations or assignments needed!
public async Task ProcessOrderAsync(int orderId)
{
// Parameters directly accessible
logger.LogInformation("Processing order {OrderId}", orderId);
var order = await repository.GetByIdAsync(orderId);
await emailService.SendConfirmationAsync(order.CustomerEmail);
}
public async Task<int> GetPendingCountAsync()
{
// All constructor parameters available throughout the class
logger.LogDebug("Counting pending orders");
return await repository.CountAsync(o => o.Status == "Pending");
}
}
// Mixing primary constructors with additional initialization
public class OrderValidator(ILogger<OrderValidator> logger, IRepository<Product> products)
{
// Can still have explicit fields
private readonly HashSet<string> _validStatuses = ["Pending", "Confirmed", "Shipped"];
// Can have additional constructors
public OrderValidator(ILogger<OrderValidator> logger)
: this(logger, new InMemoryRepository<Product>())
{
}
public async Task<bool> ValidateAsync(Order order)
{
logger.LogInformation("Validating order {OrderId}", order.Id);
if (!_validStatuses.Contains(order.Status))
return false;
// Use injected repository
foreach (var item in order.Items)
{
var product = await products.GetByIdAsync(item.ProductId);
if (product == null) return false;
}
return true;
}
}
Savings: Eliminates approximately 60% of boilerplate code in typical dependency injection scenarios, while maintaining full functionality.
Common Mistakes β οΈ
Mistake 1: Incorrect Raw String Indentation
β Wrong:
var json = """
{
"name": "Alice"
}
""";
// Result includes leading spaces because closing quotes are at column 0
β Correct:
var json = """
{
"name": "Alice"
}
""";
// Closing quotes at same indentation - whitespace trimmed properly
Rule: The closing """ determines the indentation baseline. Everything to its left is removed from all lines.
Mistake 2: Forgetting Required Members in Base Classes
β Wrong:
public class Entity
{
public required string Id { get; init; }
}
public class User : Entity
{
public required string Name { get; init; }
// Missing: need to satisfy base class required members!
}
// This won't compile:
var user = new User { Name = "Alice" }; // Error: Id not set
β Correct:
public class User : Entity
{
public required string Name { get; init; }
}
// Must initialize ALL required members from entire hierarchy
var user = new User
{
Id = "123", // Base class required member
Name = "Alice" // Derived class required member
};
Mistake 3: List Pattern Order Matters
β Wrong:
public string Classify(int[] numbers) => numbers switch
{
[.., var last] => $"Ends with {last}", // TOO BROAD - matches everything!
[1, 2, 3] => "Exact match", // Never reached!
_ => "Other"
};
β Correct:
public string Classify(int[] numbers) => numbers switch
{
[1, 2, 3] => "Exact match", // Specific patterns first
[.., var last] => $"Ends with {last}", // General patterns last
_ => "Other"
};
Principle: Pattern matching evaluates top-to-bottom. Put specific patterns before general ones.
Mistake 4: Primary Constructor Parameter Scope Confusion
β Wrong:
public class Service(ILogger logger)
{
// Trying to create a field with the same name
private readonly ILogger logger = CreateLogger(); // Error: conflicts with parameter
}
β Correct:
public class Service(ILogger logger)
{
// Use different name for explicit field
private readonly ILogger _customLogger = CreateLogger();
public void Log(string message)
{
// Can use constructor parameter
logger.LogInformation(message);
// Or use explicit field
_customLogger.LogWarning(message);
}
}
Mistake 5: Collection Expression Type Ambiguity
β Wrong:
// Ambiguous - compiler can't infer type
var items = []; // Error: cannot infer type
β Correct:
// Explicit type annotation
List<int> items = [];
// Or use with explicit initialization
var items = new List<int>([]);
// Or provide elements
var items = [1, 2, 3]; // Type inferred as int[]
Key Takeaways π―
π C# 11-12 Quick Reference
| Feature | Syntax | Primary Benefit |
|---|---|---|
| Raw String Literals | """ ... """ | No escape sequences needed |
| Required Members | required Type Property | Compile-time initialization enforcement |
| List Patterns | [first, .., last] | Declarative collection matching |
| Generic Attributes | [MyAttr | Type-safe attributes |
| File-Scoped Types | file class Helper | Encapsulation without namespaces |
| Primary Constructors | class C(int x) | Reduced boilerplate |
| Collection Expressions | [1, 2, 3] | Unified collection syntax |
| Type Aliases | using Point = (int, int); | Semantic type names |
Essential Principles π‘
Raw strings simplify complex string content - Use them for JSON, SQL, regex, XML, or any multi-line text with special characters
Required members shift validation left - Catch initialization errors at compile time instead of runtime
List patterns make collection logic declarative - Replace index checking and loop logic with pattern matching
Primary constructors reduce ceremony - Perfect for dependency injection and simple initialization
Collection expressions provide consistency - One syntax for arrays, lists, spans, and custom collections
File-scoped types improve encapsulation - Hide implementation details without namespace complexity
Type aliases enhance readability - Give semantic names to complex generic types and tuples
When to Use Each Feature π€
Use raw string literals when:
- Working with JSON, XML, SQL, or regex patterns
- Embedding multi-line text
- String contains many quotes or backslashes
Use required members when:
- Creating DTOs or API models
- Initialization is critical for correctness
- You want compile-time validation
Use list patterns when:
- Processing command-line arguments
- Parsing structured data
- Matching collection shapes
Use primary constructors when:
- Building services with dependency injection
- Creating simple data carriers
- You want less boilerplate
Use collection expressions when:
- Creating collections inline
- Combining multiple collections
- You want consistent syntax
Migration Strategy π
When adopting these features in existing codebases:
- Start with raw strings - Low risk, immediate readability gains
- Add required to new DTOs - Prevent future bugs without changing existing code
- Refactor complex if-else chains to list patterns - Improves maintainability
- Consider primary constructors in new services - Don't refactor existing working code
- Use collection expressions in new code - Consistent with modern C# style
π Further Study
- Microsoft C# 11 What's New - Official documentation of C# 11 features with detailed examples
- Microsoft C# 12 What's New - Comprehensive guide to C# 12 language improvements
- .NET Blog: Welcome to C# 12 - In-depth exploration with real-world scenarios and performance considerations