C# has changed more in the last five years than in the ten before it. A lot of production code still reads like C# 5, even on teams running .NET 10. Here are twelve features worth deliberately adopting — not because they're new, but because each one removes a category of bug or a page of boilerplate.

Key takeaways
  • Primary constructors and collection expressions cut real boilerplate — adopt them by default in new code.
  • Pattern matching and list patterns replace nested if chains with something the compiler can check for exhaustiveness.
  • Records give you value equality and immutability for free — use them for anything that represents data rather than identity.
  • Nullable reference types catch a whole class of null-reference bugs at compile time, but only if you enable them project-wide, not file-by-file.

1. Primary constructors

C# 12 lets classes (not just records) declare constructor parameters directly in the type declaration, in scope for the whole class body.

csharp
public class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
    public async Task SubmitAsync(Guid orderId)
    {
        logger.LogInformation("Submitting order {OrderId}", orderId);
        var order = await repository.GetAsync(orderId);
        order.Submit();
    }
}

2. Collection expressions

One consistent syntax for arrays, lists, and spans, with the spread operator for combining collections.

csharp
int[] numbers = [1, 2, 3];
List<string> names = ["Alice", "Bob"];
int[] combined = [..numbers, 4, 5];

3. Pattern matching with property patterns

Match on the shape of an object instead of writing a chain of if statements.

csharp
decimal DiscountFor(Order order) => order switch
{
    { Total: > 1000, Customer.IsVip: true } => 0.15m,
    { Total: > 1000 } => 0.10m,
    { Customer.IsVip: true } => 0.05m,
    _ => 0m
};

4. Records and record structs

Value equality, with-expressions, and a sensible ToString() — without writing any of it by hand.

csharp
public record Money(decimal Amount, string Currency)
{
    public Money Add(Money other) =>
        Currency != other.Currency
            ? throw new InvalidOperationException("Currency mismatch")
            : this with { Amount = Amount + other.Amount };
}

5. Required members

Force callers to set critical properties without needing a constructor overload for every combination.

csharp
public class CreateUserRequest
{
    public required string Email { get; init; }
    public required string DisplayName { get; init; }
    public string? PhoneNumber { get; init; }
}

6. Raw string literals

No more escaping quotes inside JSON, SQL, or regex embedded in C# source.

csharp
var json = """
{
  "name": "astrivo",
    "legalName": "Astrivo Technologies LLC",
  "focus": ["dotnet", "aws", "azure"]
}
""";

7. File-scoped namespaces

One less indentation level in every file, with no change in behavior.

csharp
namespace Astrivo.Orders;

public class Order
{
    // no extra indent needed
}

8. Global and implicit usings

Move common using directives into one file per project instead of repeating them everywhere.

csharp
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using Microsoft.Extensions.Logging;

9. Nullable reference types

Enable this project-wide, not opportunistically — partial adoption gives you false confidence.

xml
<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>
csharp
public class Customer
{
    public string Name { get; set; } = string.Empty; // non-nullable, must be set
    public string? MiddleName { get; set; }           // explicitly nullable
}

10. Switch expressions

Replace verbose switch statements with an expression that returns a value and forces you to handle every case.

csharp
string StatusLabel(OrderStatus status) => status switch
{
    OrderStatus.Draft => "Draft",
    OrderStatus.Submitted => "Submitted",
    OrderStatus.Shipped => "Shipped",
    _ => throw new ArgumentOutOfRangeException(nameof(status))
};

11. Init-only setters

Immutable-after-construction properties without the ceremony of a full constructor.

csharp
public class Address
{
    public string Street { get; init; } = string.Empty;
    public string City { get; init; } = string.Empty;
}

var address = new Address { Street = "1 Main St", City = "Austin" };
// address.Street = "2 Main St"; // compile error — init-only

12. List patterns

Match on the shape of a sequence, including the first/last elements and a variable-length middle section.

csharp
string Describe(int[] values) => values switch
{
    [] => "empty",
    [var single] => $"single value: {single}",
    [var first, .., var last] => $"starts with {first}, ends with {last}",
};
How to roll this outNone of these require a rewrite. Adopt them in new code and during normal refactors — a targeted style-analyzer rule set (.editorconfig) will nudge the team without a big-bang cleanup.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.