Skip to main content

Modern C# Features: C# 11 to C# 14 on Real Code – C# | Lesson 52

Published / updated

Introduction​

C# gets a new version every November, together with .NET. .NET 10 brings C# 14. Each version adds features that make common code shorter, safer, or clearer, and new codebases use them everywhere.

You have already used several: records, required, init, and file-scoped namespaces. This lesson covers the rest of the modern toolkit you will meet in job interviews and in real code. Each feature is shown on SMS data and checked on the .NET 10 SDK.

By the end of this lesson, you will be able to:

  • Write collection expressions and use the spread operator ...
  • Match the shape of data with list, property, and logical patterns.
  • Write a class with a primary constructor.
  • Use raw string literals for text with quotes, such as SQL.
  • Use C# 14's field keyword, extension members, and null-conditional assignment.

Why this matters​

Reading other people's code is most of a developer's job. A new codebase written in 2026 is full of [..], is { Status: ... }, and primary constructors; without knowing them, simple code looks like a puzzle.

Modern features also remove whole kinds of bugs. Patterns make the compiler check every case, field removes a private variable that could be changed by mistake, and raw strings stop quote-escaping errors in SQL text.

The concept​

Example: what came when​

VersionReleased withFeatures in this lessonAlready used
C# 11.NET 7, 2022List patterns, raw string literalsrequired (Lesson 44)
C# 12.NET 8, 2023Collection expressions, primary constructors
C# 13.NET 9, 2024params collectionsparams arrays (Lesson 34)
C# 14.NET 10, 2025field keyword, extension members, null-conditional assignment

The C# version comes from the project's target framework. net10.0 in the project file (Lesson 30) means C# 14 is on, with no extra setting.

Example: collection expressions​

A collection expression creates a list, array, or set with square brackets:

Example
string[] coreSubjects = ["English", "Mathematics", "Science"];
List<string> allSubjects = [.. coreSubjects, .. otherSubjects];

The same syntax works for arrays, List<T>, and many other collection types; the variable's type decides what is built. .. is the spread operator: it inserts every item of another collection. [] is an empty collection.

Example: patterns​

PatternExampleMatches when
List["Absent", ..]The first item is "Absent"
List[.., "Absent", "Absent"]The last two items are "Absent"
List[_, _, _, _, _]There are exactly five items
Property{ Status: InvoiceStatus.Paid }The object's Status is Paid
Relational> 20000A value is greater than 20,000
Logical>= 0 and <= 100Both conditions hold
Logicalnot 0The value is not 0

_ matches any single item, and .. matches any number of items, including none. Patterns combine: { Status: InvoiceStatus.PartiallyPaid, Balance: > 20000 } checks two properties at once.

Example: primary constructors​

Example
class AttendanceSummary(string studentName, int attended, int marked)
{
public decimal Percentage => marked == 0 ? 0 : Math.Round(100m * attended / marked, 2);
}

The parameters after the class name are available in the whole class body, with no fields and no constructor to write. Unlike a record's parameters, they do not become public properties; expose them yourself where needed.

One caution: the parameters are not read-only. Any method in the class could assign a new value to marked, and nothing would stop it. For values that must never change, copy the parameter into a readonly field or a { get; } property.

Example: modern features you already use​

FeatureVersionFirst used in
Switch expressionsC# 8Lesson 32, the grade scale
Nullable reference typesC# 8Lesson 35
Records and initC# 9Lessons 38 and 43
Top-level statementsC# 9Lesson 30, every Program.cs
File-scoped namespaces and implicit usingsC# 10Lesson 37
required membersC# 11Lesson 44

Modern C# is not a separate language. It is the same C# you have been writing, with a few more tools each year.

Example: target-typed new​

When the type is already written on the left, new() can leave it out on the right: List<Enrollment> enrollments = new(); is the same as new List<Enrollment>(). It is short and still states the type once, where readers look for it.

Example: the compiler checks your switch​

Switch expressions over enums are checked for missing cases. A switch on InvoiceStatus that handles only Unpaid and Paid warns: CS8509: The switch expression does not handle all possible values ... the pattern 'InvoiceStatus.PartiallyPaid' is not covered.

Even with all four statuses listed, the compiler warns CS8524 about an unnamed value, such as (InvoiceStatus)4, because any number can be cast to an enum (Lesson 43). A final _ arm answers both warnings.

That is a trade-off. With a quiet default such as _ => "Unknown", a status added next year silently falls into the default. Many teams list every status and end with _ => throw new ArgumentOutOfRangeException(...), so an unexpected value fails loudly instead.

Apply it to the SMS​

Step 1: the field keyword in Person​

Since Lesson 38, Person kept two private fields only so the setters could check names. C# 14's field keyword refers to the property's own hidden backing field, so the private fields go away:

src/SchoolApp.Core/Entities/Person.cs
namespace SchoolApp.Core.Entities;

// Lesson 39: what students, teachers, and guardians have in common.
// Each still has its own table in SchoolDb; inheritance shares C# code, not tables.
// Lesson 41: abstract (there is no plain "person" in the school).
// Lesson 44: inherits the audit and soft-delete columns from SoftDeletableEntity.
// Lesson 52: FirstName and LastName use the C# 14 field keyword instead of private fields.
public abstract class Person : SoftDeletableEntity
{
public int Id { get; set; }

// field is the compiler-made backing field: no _firstName needed.
public string FirstName { get; set => field = RequireText(value, nameof(FirstName)); } = "";

public string LastName { get; set => field = RequireText(value, nameof(LastName)); } = "";

public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}

public string FullName => $"{FirstName} {LastName}";

// Lesson 41: every kind of person must say what role it has.
public abstract string Role { get; }

// Lesson 40: virtual lets each derived class describe itself in its own way.
public virtual string Describe() => FullName;

// object.ToString() is virtual too; overriding it changes what printing an object shows.
public override string ToString() => Describe();

// protected: usable here and in every class that inherits from Person.
protected static string RequireText(string value, string propertyName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{propertyName} is required.", propertyName);
}
return value.Trim();
}
}

The behaviour is identical: names are still trimmed and checked. = "" sets the starting value of the hidden field.

Step 2: the program​

Show src/SchoolApp.Console/Program.cs
src/SchoolApp.Console/Program.cs
// Lesson 52: modern C# (versions 11 to 14) on SMS data.
using SchoolApp.Core.Entities;
using SchoolApp.Core.Enums;

// 1. Collection expressions and the spread operator (C# 12).
string[] coreSubjects = ["English", "Mathematics", "Science"];
string[] otherSubjects = ["Social Studies", "Hindi", "Computer Science"];
List<string> allSubjects = [.. coreSubjects, .. otherSubjects];
Console.WriteLine($"{allSubjects.Count} subjects: {string.Join(", ", allSubjects)}");

// 2. List patterns (C# 11): match the shape of a week's register.
string[][] weeks =
[
["Present", "Present", "Late", "Present", "Present"],
["Absent", "Present", "Present", "Present", "Present"],
["Present", "Present", "Present", "Absent", "Absent"],
["Present", "Present", "Present"],
];
foreach (string[] week in weeks)
{
string summary = week switch
{
["Absent", ..] => "absent on Monday",
[.., "Absent", "Absent"] => "absent the last two days",
[_, _, _, _, _] => "a full five-day week",
_ => $"a short week of {week.Length} days"
};
Console.WriteLine($"Week: {summary}");
}

// 3. Property patterns: decide on the invoice's own properties.
FeeInvoice invoice = new FeeInvoice("INV-2026-000002", 45500m);
invoice.RecordPayment(20000m);
string action = invoice switch
{
{ Status: InvoiceStatus.Paid } => "send a thank-you note",
{ Status: InvoiceStatus.PartiallyPaid, Balance: > 20000 } => "large balance: call the family",
{ Status: InvoiceStatus.PartiallyPaid } => "send a reminder",
_ => "send the invoice"
};
Console.WriteLine($"{invoice.InvoiceNumber}, balance {invoice.Balance}: {action}");

// 4. Logical patterns: and, or, not.
decimal mark = 67m;
Console.WriteLine($"67 is a valid mark: {mark is >= 0 and <= 100}; needs review: {mark is < 33 or > 98}; not zero: {mark is not 0}");

// 5. A primary constructor (C# 12): the parameters are available in the whole class.
AttendanceSummary aisha = new AttendanceSummary("Aisha Reddy", 22, 27);
Console.WriteLine(aisha);

// 6. A raw string literal (C# 11): quotes and line breaks need no escaping. Module 9 runs SQL like this.
string sql = """
SELECT FirstName, LastName
FROM dbo.Students
WHERE AdmissionNumber = @AdmissionNumber;
""";
Console.WriteLine($"SQL has {sql.Split('\n').Length} lines and starts with: {sql.Split('\n')[0]}");

// 7. The field keyword (C# 14): Person.FirstName still trims and checks its value.
Student diya = new Student("ADM-2017-0002", " Diya ", "Patel", new DateOnly(2011, 8, 23), Gender.Female);
Console.WriteLine($"Stored first name: [{diya.FirstName}]");

// 8. Extension members (C# 14): an extension property on Student.
Console.WriteLine($"{diya.FullName}: initials {diya.Initials}");

// 9. Null-conditional assignment (C# 14): assign only when the object exists.
Guardian? notLoaded = null;
notLoaded?.Email = "nobody@nexcodingschool.example";
Console.WriteLine($"Assignment skipped safely: {notLoaded is null}");

class AttendanceSummary(string studentName, int attended, int marked)
{
public decimal Percentage => marked == 0 ? 0 : Math.Round(100m * attended / marked, 2);

public override string ToString() => $"{studentName}: {attended} of {marked} days ({Percentage}%)";
}

static class StudentExtensions
{
extension(Student student)
{
public string Initials => $"{student.FirstName[0]}{student.LastName[0]}";
}
}
Output
6 subjects: English, Mathematics, Science, Social Studies, Hindi, Computer Science
Week: a full five-day week
Week: absent on Monday
Week: absent the last two days
Week: a short week of 3 days
INV-2026-000002, balance 25500: large balance: call the family
67 is a valid mark: True; needs review: False; not zero: True
Aisha Reddy: 22 of 27 days (81.48%)
SQL has 3 lines and starts with: SELECT FirstName, LastName
Stored first name: [Diya]
Diya Patel: initials DP
Assignment skipped safely: True

Example: reading a register with list patterns​

Four weeks of attendance were matched against list patterns. The order of the arms matters, as in Lesson 32: the second week begins with "Absent", so it matches ["Absent", ..] before the five-item pattern is even tried.

The last week has only three days, so it falls through to _. A school week cut short by a holiday is a real case, and the pattern names it clearly instead of hiding it in index arithmetic such as week[week.Length - 1].

Example: deciding on an invoice​

Diya's invoice has paid 20,000 of 45,500. The property pattern { Status: InvoiceStatus.PartiallyPaid, Balance: > 20000 } matches, because 25,500 is still due, so the school should call the family rather than send another letter.

The rule reads almost like the sentence a fee clerk would say out loud at the counter. Compare it with nested if statements checking invoice.Status and invoice.Balance separately.

Example: a primary constructor with real data​

AttendanceSummary computes 22 of 27 days as 81.48%, Aisha Reddy's real July attendance from Extra 1. The whole class is four lines, yet it has a constructor, a computed property, and a ToString.

Example: raw strings for SQL​

A raw string starts and ends with three double quotes. Inside, quotes and backslashes need no escaping, and the indentation of the closing """ is removed from every line. The SQL keeps its natural layout, three clean lines, exactly as you would write it in SSMS.

In Module 9, this is how SQL text is written in C#. $"""...""" adds interpolation, but SQL values must never be glued in that way; they go in parameters such as @AdmissionNumber, to prevent SQL injection (Lesson 22).

Example: extension members​

Lesson 45 wrote extension methods. C# 14's extension block adds extension properties too:

Example
static class StudentExtensions
{
extension(Student student)
{
public string Initials => $"{student.FirstName[0]}{student.LastName[0]}";
}
}

diya.Initials returns "DP", as if Student had the property itself. The block can hold several methods and properties for the same type.

Example: null-conditional assignment​

notLoaded?.Email = "..." assigns only when notLoaded is not null. Before C# 14, this needed an if (notLoaded is not null) block around the assignment. Here the guardian was not loaded, so the assignment is skipped without an exception.

Common mistakes​

  1. Pattern arms in the wrong order. A general arm placed first catches everything. Fix: specific patterns first, _ last.
  2. Treating primary constructor parameters as properties. They are not visible outside. Fix: add properties, or use a record if every parameter should be public.
  3. Interpolating values into SQL raw strings. That invites SQL injection. Fix: use parameters.
  4. Using the newest feature everywhere at once. Mixed styles confuse a team. Fix: agree which features to adopt, and apply them consistently.
  5. Expecting C# 14 features on an older target framework. field fails on net8.0. Fix: check the project's TargetFramework.

In real projects​

From 16 years of .NET projects

When a team I worked with upgraded to a new .NET version, one developer converted half the codebase to new syntax in a single pull request. It was correct, and nearly impossible to review.

We agreed on a rule: use modern features in new and changed code, and convert old code only when touching it for another reason. The codebase modernised steadily, one small change at a time.

New features are tools for clarity. If a feature makes a line harder for the next person to read, it is the wrong tool for that line.

Best practices​

  • Use collection expressions for new lists and arrays.
  • Use patterns when they make a decision read like the business rule.
  • Use primary constructors for small classes; records for data that should be public.
  • Write SQL in raw strings, and always pass values as parameters.
  • Adopt new features as a team, consistently, in new and changed code.

Recap​

  • Collection expressions build collections with [...]; .. spreads another collection in.
  • List, property, relational, and logical patterns match the shape and values of data.
  • Primary constructors make parameters available in the whole class body.
  • Raw string literals keep SQL readable with no escaping.
  • C# 14: field replaces hand-written backing fields, extension blocks add properties, and ?.= assigns safely.
  • Person now uses field, with the same behaviour and less code.

Homework​

Library module task: Rewrite library code with modern C# features.

  1. Create a list of issues with a collection expression.
  2. Write an Issue class with a primary constructor.
  3. Use a property pattern to say "returned", "overdue", or "on loan".
  4. Use a list pattern on a borrowing history.
  5. Make Book.Title trim its value with field.
  6. Add an extension method IsOverdueOn.

A correct result looks like:

Output
Wings of Fire (Aarav Sharma): returned
The Jungle Book (Advika Sharma): overdue
Kabir started with Wings of Fire, latest The Jungle Book
Book title: [Malgudi Days]
Show the solution
library-assignment/052-library-modern.cs
// Library homework, Lesson 52: modern C# features on library data.
// Run with: dotnet run library-assignment/052-library-modern.cs
DateOnly today = new DateOnly(2026, 7, 28);

// Collection expression + primary constructors.
List<Issue> issues =
[
new Issue("Wings of Fire", "Aarav Sharma", new DateOnly(2026, 7, 15), new DateOnly(2026, 7, 14)),
new Issue("The Jungle Book", "Advika Sharma", new DateOnly(2026, 7, 20), null),
];

foreach (Issue issue in issues)
{
// Property patterns, with an extension property from C# 14.
string state = issue switch
{
{ ReturnedOn: not null } => "returned",
_ when issue.IsOverdueOn(today) => "overdue",
_ => "on loan"
};
Console.WriteLine($"{issue.Title} ({issue.Student}): {state}");
}

// List pattern on a student's borrowing history.
string[] history = ["Wings of Fire", "Malgudi Days", "The Jungle Book"];
string note = history switch
{
[] => "has never borrowed",
[var only] => $"has borrowed only {only}",
[var first, .., var last] => $"started with {first}, latest {last}"
};
Console.WriteLine($"Kabir {note}");

// field keyword: the title is always trimmed.
Book book = new Book { Title = " Malgudi Days " };
Console.WriteLine($"Book title: [{book.Title}]");

class Issue(string title, string student, DateOnly dueOn, DateOnly? returnedOn)
{
public string Title => title;
public string Student => student;
public DateOnly DueOn => dueOn;
public DateOnly? ReturnedOn => returnedOn;
}

class Book
{
public string Title { get; set => field = value.Trim(); } = "";
}

static class IssueExtensions
{
extension(Issue issue)
{
public bool IsOverdueOn(DateOnly date) => issue.ReturnedOn is null && date > issue.DueOn;
}
}

_ when issue.IsOverdueOn(today) is a case guard: the _ matches anything, and the when condition decides whether the arm applies.

Quiz​

  1. What does [.. coreSubjects, .. otherSubjects] create?

    1. An array of two arrays
    2. One collection with every item of both
    3. A range
    4. An error
    Show answer

    Answer: B. One collection with every item of both The spread operator inserts every item of each collection.

  2. Which list pattern matches a week whose last two days were absences?

    1. ["Absent", ..]
    2. [.., "Absent", "Absent"]
    3. [_, _, _, _, _]
    4. []
    Show answer

    Answer: B. [.., "Absent", "Absent"] .. matches any number of items before the last two.

  3. What does the C# 14 field keyword refer to?

    1. A database column
    2. The property's hidden backing field
    3. A static field
    4. A record parameter
    Show answer

    Answer: B. The property's hidden backing field It removes the need for a hand-written private field.

  4. How do primary constructor parameters differ from record parameters?

    1. They are the same
    2. They are available in the class body but do not become public properties
    3. They are always static
    4. They cannot be used in methods
    Show answer

    Answer: B. They are available in the class body but do not become public properties Records turn parameters into public properties; classes do not.

  5. Why should SQL values not be interpolated into a raw string?

    1. Raw strings cannot interpolate
    2. It risks SQL injection; use parameters instead
    3. It is slower
    4. SQL Server rejects it
    Show answer

    Answer: B. It risks SQL injection; use parameters instead Parameters keep values separate from the SQL text.

Interview questions​

Questions are grouped by experience level. Click a question to see a model answer. All questions: Advanced C# interview questions.

Fresher (0-2 yrs)​

F What is a collection expression? concept

C# 12 syntax for creating collections with square brackets: string[] subjects = ["English", "Science"];. It works for arrays, lists, and other collection types, and .. spreads another collection's items in.

See the example · Permalink

F What is a raw string literal, and when is it useful? concept

Text between three double quotes ("""), where quotes and backslashes need no escaping and the closing quotes' indentation is removed. It suits SQL, JSON, and other text with quotes; values still belong in SQL parameters.

See the example · Permalink

F What does the C# 14 field keyword do? concept

Inside a property accessor, field refers to the property's compiler-made backing field, so a property with a custom setter, such as trimming or validation, no longer needs a hand-written private field.

See the example · Permalink

Mid (3-5 yrs)​

M What are list patterns? concept

Patterns that match the shape of a sequence: ["Absent", ..] (first item is Absent), [.., "Absent", "Absent"] (last two), [_, _, _] (exactly three items). _ matches one item and .. any number of items.

See the example · Permalink

M How do primary constructors on classes differ from records? concept

Both declare parameters after the type name. A record turns them into public init-only properties with value equality. A class with a primary constructor only makes the parameters available inside the class body; they are not properties, and they are not read-only.

See the example · Permalink

M What are C# 14 extension members? concept

An extension(Type receiver) { ... } block inside a static class that can declare extension properties as well as methods, such as student.Initials. Before C# 14, only extension methods were possible.

See the example · Permalink

Senior (6-9 yrs)​

S A developer converted half the codebase to new C# syntax in one pull request. How do you handle it? scenario

Ask to split it: a large mechanical change hides real changes and is hard to review. Agree on which features the team adopts, apply them in new and changed code, and convert old code gradually or with automated, separately reviewed refactorings.

How to answer: Balance modernization against reviewability and risk. Mention coding standards, analyzers or editorconfig rules, and keeping behavior changes separate from style changes.

See the example · Permalink

S Should a switch over an enum end with a default arm? What are the trade-offs? design

Without _, the compiler warns about missing statuses (CS8509) and unnamed values (CS8524), which helps when new statuses are added. A quiet default hides new statuses. A common choice is to list every status and end with _ => throw, so unexpected values fail loudly.

How to answer: Explain both warnings, the risk of silent defaults, and the throwing default as a middle path.

See the example · Permalink

Architect / Lead (10+ yrs)​

A How would you keep a large codebase current with new .NET and C# versions? design

Upgrade to each LTS release on a plan, update the target framework and packages, fix analyzer warnings, and adopt new language features through agreed coding standards in new and changed code. Automated tests make upgrades safe, and editorconfig or analyzers keep the style consistent.

How to answer: Cover the upgrade cadence (LTS), risk control (tests, staged rollout), and team practices for adopting features. Mention support end dates as the business driver.

See the example · Permalink

Resources​

  • Code for this lesson: git tag lesson-52-modern-csharp (code repository coming soon)
  • Files: src/SchoolApp.Core/Entities/Person.cs, src/SchoolApp.Console/Program.cs, library-assignment/052-library-modern.cs
  • Video: coming soon

What's next​

Every program so far has waited for each step to finish completely before starting the next. Next: async and await, so the SMS can send many fee notices at once and stay responsive while it waits. Lesson 53: async and await

Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.