# Modern C# Features

Course: [C# Advanced](https://www.devtutorials.ai/learn/advanced-csharp) · Section: Modern C# Features
Source: https://www.devtutorials.ai/learn/advanced-csharp/modern-csharp/modern-csharp-features

> Write less code with fewer mistakes: required members, primary constructors, ranges and indexes, collection expressions, raw strings and more.

## Introduction to Modern C# Features

Think about an old car and a new one.

Both get you from home to work. But the new one has power steering, a reversing camera and keyless entry. Nothing it does is impossible in the old car; it is just **easier, safer and quicker**.

C# has changed the same way. Every year a new version adds small features that remove boring code and prevent common bugs. Code written in C# 12 or 13 often looks very different from C# 6 code, even though it does the same job.

**In simple words:** modern C# features are shortcuts and safety checks that let you write less code with fewer mistakes.

This lesson collects the features you will see most in modern code:

| Feature | Version | Removes |
|---|---|---|
| Target-typed `new` | C# 9 | Writing the type twice |
| `init`, records | C# 9 | Boilerplate for immutable data |
| Global usings, file-scoped namespaces | C# 10 | Repeated `using` lines and one level of indentation |
| Raw string literals | C# 11 | Escaping quotes and backslashes |
| `required` members | C# 11 | Objects created with missing data |
| Primary constructors, collection expressions | C# 12 | Constructor boilerplate, verbose collection creation |

**Easy Way to Remember:** modern C# is the **same language with less typing**.

## What You Will Learn

1. Required members and primary constructors.
2. Anonymous types.
3. The range operator `..` and the index operator `^`.
4. Collection expressions `[ ]`.
5. Global using directives and file-scoped namespaces.
6. Raw string literals.
7. Target-typed `new` and improved object initialization.
8. Modern C# coding practices.

Most examples run in your browser. Global usings and file-scoped namespaces are about how a whole **project** is organised across several files, so they are shown as plain code.

## Start With a Working Example

Press **Run**.

```csharp runnable title="Modern C# in a few lines"
using System;
using System.Collections.Generic;

List<Product> products =
[
    new("Desk lamp", 899m),
    new("Notebook", 120m),
    new("Pen", 25m),
    new("Backpack", 1499m),
];

Product[] cheapest = products.ToArray()[^2..];   // last two items
var summary = new { Count = products.Count, First = products[0].Name };

Console.WriteLine($"{summary.Count} products, first is {summary.First}");
foreach (Product p in cheapest)
    Console.WriteLine(p.Label());

class Product(string name, decimal price)
{
    public string Name { get; } = name;
    public string Label() => $"{name}: Rs. {price}";
}
```

That small program uses a **collection expression** (`[ ... ]`), **target-typed `new`** (`new("Pen", 25m)`), the **index and range operators** (`[^2..]`), an **anonymous type** (`new { Count = ... }`) and a **primary constructor** (`class Product(string name, decimal price)`). The rest of the lesson explains each one.

### Try This

Change `[^2..]` to `[..2]`. Now the first two products are printed instead of the last two.

## Required Members

A **`required`** property **must** be set when the object is created. If you forget it, the code does not compile.

Before C# 11, nothing stopped this:

```text
var user = new User();          // Email forgotten
SendWelcome(user.Email);        // null, discovered only at run time
```

With `required`, the compiler catches it immediately:

```csharp runnable title="required forces callers to set the value"
using System;

var user = new User { Name = "Asha", Email = "asha@example.com" };
Console.WriteLine($"{user.Name} <{user.Email}>, role {user.Role}");

// var broken = new User { Name = "Ravi" };
// error CS9035: Required member 'User.Email' must be set

class User
{
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string Role { get; init; } = "Member";   // optional, has a default
}
```

| Without `required` | With `required` |
|---|---|
| Missing data is found at run time, as a `null` | Missing data stops you at compile time, before you run the code |
| Needs a constructor to force values | Works with simple `{ ... }` initializers |
| Easy to add a property and forget to set it everywhere | Every place that creates the object is checked automatically |

`required` works with `init` and `set` properties, and with fields. A constructor marked `[SetsRequiredMembers]` tells the compiler that it sets them all itself.

## Primary Constructors

A **primary constructor** puts the constructor parameters right after the class name. It was added for records in C# 9 and for normal classes and structs in C# 12.

Old style:

```text
public class OrderService
{
    private readonly IEmailSender _email;
    private readonly ILogger _log;

    public OrderService(IEmailSender email, ILogger log)
    {
        _email = email;
        _log = log;
    }
}
```

With a primary constructor:

```text
public class OrderService(IEmailSender email, ILogger log)
{
    // email and log can be used anywhere inside the class
}
```

```csharp runnable title="A class with a primary constructor"
using System;

var account = new BankAccount("Asha", 1000m);
account.Deposit(250m);
account.Withdraw(100m);
Console.WriteLine(account.Summary());

var greeter = new Greeter("Namaste");
Console.WriteLine(greeter.Greet("Ravi"));

class BankAccount(string owner, decimal openingBalance)
{
    private decimal _balance = openingBalance;     // parameter used to set a field

    public string Owner => owner;                  // parameter used in a property
    public void Deposit(decimal amount) => _balance += amount;
    public void Withdraw(decimal amount) => _balance -= amount;
    public string Summary() => $"{Owner}: {_balance}";
}

class Greeter(string greeting)
{
    public string Greet(string name) => $"{greeting}, {name}!";
}
```

| | Primary constructor in a `record` | Primary constructor in a `class` or `struct` |
|---|---|---|
| Creates public properties | **Yes** | **No** |
| Parameters usable in the body | Yes | Yes |
| Typical use | Data | Services that receive their dependencies |

In a **class**, primary constructor parameters are **not** properties, and they can be changed inside the class. If you need a public property or a value that never changes, assign the parameter to a property or a `readonly` field, as `_balance` and `Owner` do above.

## Anonymous Types

An **anonymous type** is a small object created on the spot **without** declaring a class. You write `new { ... }` and list the properties. Once created, you cannot change the properties.

```csharp runnable title="Anonymous types for quick, local results"
using System;
using System.Linq;

var point = new { X = 3, Y = 4 };
Console.WriteLine(point);
Console.WriteLine($"X + Y = {point.X + point.Y}");

string[] names = { "Asha", "Ravi", "Meenakshi" };
var lengths = names.Select(n => new { Name = n, Letters = n.Length });

foreach (var item in lengths)
    Console.WriteLine($"{item.Name} has {item.Letters} letters");

var same = new { X = 3, Y = 4 };
Console.WriteLine($"Equal by value: {point.Equals(same)}");
```

Facts about anonymous types:

- They are **fixed after creation**: you cannot change `point.X` after creation.
- They compare **by value**, like records. Two anonymous types with the same properties are equal.
- They have **no name** you can write, so you must use `var`.
- They are meant for **local** use, especially LINQ `Select` results. To return data from a method, use a **record** or a **tuple** instead.

## Range Operator (..)

The **range operator `..`** extracts a **slice** (a section) from an array, string or span. You give it a starting position and an ending position.

| Range | Means |
|---|---|
| `a..b` | From index `a` up to, **but not including**, `b` |
| `a..` | From `a` to the end |
| `..b` | From the start up to (not including) `b` |
| `..` | Everything (the entire collection) |

```csharp runnable title="Slicing arrays and strings"
using System;

int[] marks = { 55, 67, 72, 81, 90, 98 };

Console.WriteLine(string.Join(", ", marks[1..4]));   // index 1, 2, 3
Console.WriteLine(string.Join(", ", marks[..2]));    // first two
Console.WriteLine(string.Join(", ", marks[4..]));    // from index 4 to the end

string phone = "9876543210";
Console.WriteLine($"Area: {phone[..3]}, rest: {phone[3..]}");

Range middle = 2..5;                                // a Range can be stored
Console.WriteLine(string.Join(", ", marks[middle]));
```

The end of a range is **exclusive** (not included), just like `i < length` in a loop. So `marks[1..4]` gives you 3 items: at positions 1, 2, and 3. Position 4 is not included.

## Index Operator (^)

The **index operator `^`** counts **from the end** of a collection. `^1` means the last item, `^2` means the second-to-last item, and so on.

| Index | Means | Same as |
|---|---|---|
| `^1` | Last item | `arr[arr.Length - 1]` |
| `^2` | Second-to-last item | `arr[arr.Length - 2]` |
| `^0` | Just past the end (only useful in ranges) | `arr.Length` |

You can combine `^` and `..` together: `[^3..]` means "the last three items".

```csharp runnable title="Counting from the end"
using System;
using System.Collections.Generic;

string[] queue = { "Asha", "Ravi", "Meena", "Kiran", "Divya" };

Console.WriteLine($"Last in line: {queue[^1]}");
Console.WriteLine($"Second last:  {queue[^2]}");
Console.WriteLine($"Last three:   {string.Join(", ", queue[^3..])}");
Console.WriteLine($"All but first and last: {string.Join(", ", queue[1..^1])}");

string file = "report-2026.pdf";
Console.WriteLine($"Extension: {file[^3..]}");

var list = new List<int> { 10, 20, 30 };
Console.WriteLine($"Last in list: {list[^1]}");
```

`^0` points **one position past** the last item, so it is only useful in ranges. Using `queue[^0]` to read a value throws `IndexOutOfRangeException`; you must use `^1` for the last item.

## Collection Expressions

**Collection expressions** (C# 12) create collections using square brackets `[ ... ]`. The same syntax works for arrays, lists, sets and more: the compiler automatically builds the type you need.

```text
int[] a = [1, 2, 3];
List<string> b = ["x", "y"];
HashSet<int> c = [1, 2, 2, 3];
```

The **spread** element `..` takes all items from another collection and puts them into the new one.

```csharp runnable title="One syntax for arrays, lists and sets"
using System;
using System.Collections.Generic;

int[] primes = [2, 3, 5, 7];
List<string> fruits = ["apple", "mango"];
HashSet<int> unique = [1, 2, 2, 3, 3, 3];
int[] empty = [];

int[] morePrimes = [.. primes, 11, 13];            // spread: copy all items, then add more
List<string> basket = [.. fruits, "banana", .. new[] { "kiwi" }];

Console.WriteLine(string.Join(", ", primes));
Console.WriteLine($"Unique count: {unique.Count}");
Console.WriteLine($"Empty length: {empty.Length}");
Console.WriteLine(string.Join(", ", morePrimes));
Console.WriteLine(string.Join(", ", basket));

Console.WriteLine(Total([10, 20, 30]));   // pass a collection directly

static int Total(List<int> numbers)
{
    int sum = 0;
    foreach (int n in numbers) sum += n;
    return sum;
}
```

| Old way | Collection expression |
|---|---|
| `new int[] { 1, 2, 3 }` | `[1, 2, 3]` |
| `new List<string> { "a", "b" }` | `["a", "b"]` |
| `Array.Empty<int>()` | `[]` |
| `a.Concat(b).ToArray()` | `[.. a, .. b]` |

## Global Using Directives

Most files in a project start with the same `using` statements. A **global using** (C# 10) is written **once**, in a single file, and applies automatically to **every** other file in the project.

```text
// File: GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using MyShop.Models;
```

Now no other file needs those lines at the top.

Modern project files also turn on **implicit usings**, which adds the most common namespaces automatically without you writing anything:

```text
<!-- In the .csproj file -->
<ImplicitUsings>enable</ImplicitUsings>
```

For a console app this includes `System`, `System.IO`, `System.Linq`, `System.Collections.Generic`, `System.Net.Http` and `System.Threading.Tasks`. This is why a new console project can call `Console.WriteLine` and `List<T>` without any `using` lines.

| | Normal `using` | `global using` |
|---|---|---|
| Applies to | One file only | Every file in the project |
| Where to put it | Top of each file | Usually one file, such as `GlobalUsings.cs` |
| Good for | Namespaces one file needs | Namespaces almost every file needs |

## File-Scoped Namespaces

A **file-scoped namespace** (C# 10) ends with a semicolon instead of braces. Everything in the file belongs to that namespace, and the whole file needs less indentation.

Old style:

```text
namespace MyShop.Models
{
    public class Product
    {
        public string Name { get; set; } = "";
    }
}
```

File-scoped:

```text
namespace MyShop.Models;

public class Product
{
    public string Name { get; set; } = "";
}
```

Rules:

- Only **one** file-scoped namespace per file.
- It must come **before** any types in the file (after the `using` lines).
- It cannot be mixed with top-level statements in the same file, so use it in class files, not in `Program.cs`.

Almost all new C# code uses file-scoped namespaces. They are shown as plain code here because the playground runs a single file of top-level statements.

## Raw String Literals

A **raw string literal** (C# 11) starts and ends with **three or more double quotes** `"""`. Inside it, all characters are taken literally: quotes, backslashes and new lines do not need escaping.

They are perfect for JSON, SQL, HTML, regular expressions and Windows file paths.

```csharp runnable title="JSON and paths without escaping"
using System;

string oldJson = "{ \"name\": \"Asha\", \"age\": 29 }";

string json = """
    {
      "name": "Asha",
      "age": 29
    }
    """;

string path = """C:\Users\asha\Documents\report.pdf""";

string name = "Ravi";
int score = 92;
string message = $"""
    Student: "{name}"
    Score:   {score}/100
    """;

Console.WriteLine(oldJson);
Console.WriteLine(json);
Console.WriteLine(path);
Console.WriteLine(message);
```

Rules for multi-line raw strings:

- The opening `"""` is followed by a new line, and the closing `"""` sits on its own line.
- The indentation of the **closing** `"""` is removed from every line, so you can indent the text nicely in your code without changing the output.
- Put `$` in front for interpolation. If the text itself contains `{` `}`, use `$$"""` instead and then write `{{value}}` for the holes.

## Target-Typed new Expressions

When the type is already clear from the left side of the assignment, you can leave it out after `new` (C# 9). This is called **target-typed `new`**.

```text
Dictionary<string, List<int>> scores = new Dictionary<string, List<int>>();  // old
Dictionary<string, List<int>> scores = new();                               // target-typed
```

```csharp runnable title="new() where the type is obvious"
using System;
using System.Collections.Generic;

Dictionary<string, List<int>> scores = new();
scores["Asha"] = new() { 78, 85 };
scores["Ravi"] = new() { 64 };

DateTime start = new(2026, 10, 1);
Order order = new("A-77", start);

PrintOrder(new("B-12", new(2026, 12, 25)));   // the parameter type tells new() what to build

Console.WriteLine($"Asha has {scores["Asha"].Count} scores");
Console.WriteLine($"{order.Id} on {order.Date:dd MMM yyyy}");

static void PrintOrder(Order o) => Console.WriteLine($"Printing {o.Id} for {o.Date:dd MMM}");

record Order(string Id, DateTime Date);
```

| Use `var` when... | Use target-typed `new()` when... |
|---|---|
| The right side shows the type clearly: `var list = new List<int>();` | The left side shows the type: fields, properties, parameters |
| A local variable | A field such as `private readonly List<Order> _orders = new();` |

Both remove repetition. Choose whichever keeps the type **visible one time**.

## Improved Object Initialization

Creating objects has become safer and more concise. These features work together:

| Feature | What it adds |
|---|---|
| Object initializer `{ Prop = value }` | Set properties right when the object is created |
| `init` accessor | Properties that can only be set during initialization, then never changed |
| `required` | Properties that **must** be set when the object is created |
| Target-typed `new()` | No need to repeat the type name |
| `with` expression | Create a copy with some properties changed to new values |
| Nested initializers | Set child object properties and fill collections in one line |

```csharp runnable title="Safe, readable object creation"
using System;
using System.Collections.Generic;

Invoice invoice = new()
{
    Number = "INV-2026-041",
    Customer = new() { Name = "Asha", City = "Chennai" },
    Lines = { new("Desk lamp", 899m), new("Pen", 25m) },   // adds to the existing list
};

Invoice corrected = invoice with { Number = "INV-2026-041-A" };

Console.WriteLine($"{invoice.Number} for {invoice.Customer.Name} ({invoice.Customer.City})");
Console.WriteLine($"Lines: {invoice.Lines.Count}, total {invoice.Total}");
Console.WriteLine($"Corrected copy: {corrected.Number}");

record Line(string Item, decimal Price);

record Customer
{
    public required string Name { get; init; }
    public string City { get; init; } = "Unknown";
}

record Invoice
{
    public required string Number { get; init; }
    public required Customer Customer { get; init; }
    public List<Line> Lines { get; } = new();
    public decimal Total
    {
        get
        {
            decimal sum = 0;
            foreach (Line l in Lines) sum += l.Price;
            return sum;
        }
    }
}
```

`Lines = { ... }` with no `new` **adds items** to the list that the property already created. The compiler checks that `Number` and `Customer` are set, because they are `required`.

## Modern C# Coding Practices

Modern C# is not just new syntax. It also comes with habits that make code safer and easier to read.

| Practice | Example |
|---|---|
| Turn on nullable reference types | `<Nullable>enable</Nullable>`; use `string?` for values that can be null |
| Prefer records and `init` for data | `record Address(string City, string Pin);` |
| Use `required` instead of hoping callers remember | `public required string Email { get; init; }` |
| Use primary constructors for dependency injection | `class OrderService(IOrderRepo repo)` |
| Use collection expressions | `List<int> ids = [1, 2, 3];` |
| Use pattern matching for decisions | `price switch { < 100 => ..., _ => ... }` |
| Use file-scoped namespaces and global usings | `namespace Shop.Models;` |
| Use raw strings for JSON, SQL and regex | `"""{ "id": 1 }"""` |
| Use `is null` / `is not null` | `if (user is null) return;` |
| Use `async`/`await` for I/O | `await File.ReadAllTextAsync(path);` |
| Keep methods small, names clear | `CalculateShipping()`, not `Calc2()` |

```csharp runnable title="Old style and modern style, side by side"
using System;
using System.Collections.Generic;
using System.Linq;

// Old style
List<OldPerson> oldPeople = new List<OldPerson>();
oldPeople.Add(new OldPerson("Asha", 29));
oldPeople.Add(new OldPerson("Ravi", 17));
foreach (OldPerson p in oldPeople)
{
    string label;
    if (p.Age >= 18) label = "adult"; else label = "minor";
    Console.WriteLine(p.Name + " is an " + label);
}

// Modern style
List<Person> people = [new("Asha", 29), new("Ravi", 17)];
foreach (var (name, age) in people)
    Console.WriteLine($"{name} is {(age >= 18 ? "an adult" : "a minor")}");

Console.WriteLine($"Adults: {people.Count(p => p is { Age: >= 18 })}");

class OldPerson
{
    public string Name;
    public int Age;
    public OldPerson(string name, int age) { Name = name; Age = age; }
}

record Person(string Name, int Age);
```

Look closely at the old-style output: "Ravi is an minor". The string-gluing version made a grammar mistake that is easy to miss. The modern version, with interpolation and a pattern, is shorter **and** reads like the sentence it prints.

You do not need to rewrite old code just to use new features. Use modern features in new code, and when you are already changing a file for another reason, tidy up the part you touch.

## Common mistakes

| Mistake | Wrong | Correct | Why |
|---|---|---|---|
| Reading the last item with `^0` | `arr[^0]` | `arr[^1]` | `^0` is one past the end |
| Expecting an inclusive range end | `arr[1..3]` for 3 items | `arr[1..4]` | The end index is excluded |
| Forgetting a required member | `new User { Name = "A" }` | Set every `required` property | Compile error CS9035 |
| Returning an anonymous type | `return new { Name, Age };` | `return new Person(Name, Age);` | Callers cannot name the type |
| Primary-constructor param as property | `account.owner` from outside | Add `public string Owner => owner;` | Class parameters are not properties |
| Escaping inside raw strings | `"""C:\\Temp"""` | `"""C:\Temp"""` | Raw strings need no escaping |
| File-scoped namespace with braces | `namespace App; { ... }` | `namespace App;` and no braces | It applies to the whole file |

## What to Remember

- Modern C# features remove boilerplate and catch mistakes at compile time.
- `required` properties must be set when an object is created.
- Primary constructors put parameters after the class name; in classes they are not properties.
- Anonymous types (`new { ... }`) are fixed after creation, compare by value, and are for local use.
- `a..b` slices from `a` up to, but not including, `b`; `^1` is the last item.
- Collection expressions `[1, 2, 3]` and the spread `..` build any collection type.
- `global using` applies a `using` to every file; file-scoped namespaces (`namespace X;`) remove a level of indentation.
- Raw string literals `"""..."""` need no escaping.
- Target-typed `new()` removes repeated type names; initializers, `init`, `required` and `with` make object creation safe.
- Use modern features in new code, and tidy old code when you touch it.

Next: Reflection
