← Back to Articles         
C# 9.0 – Important Features Explained

C# 9.0 – Important Features Explained

Published on 12 Oct 2026     13 min read C#
C# 9 - Features

C# 9.0 was released in November 2020 alongside .NET 5. It introduced features that simplify object creation, improve immutability, reduce boilerplate code, and enhance pattern matching.

We will cover five important features with detailed explanations, example programs, execution flows, comparisons, and interview key points.

 

1. Records

What are Records in C#?

A record is a reference type introduced in C# 9.0, designed primarily for representing data.

Records are especially useful when working with immutable data and when two objects should be compared based on their values rather than their references.

By default, classes use reference equality, whereas records provide value-based equality.

Example Program

using System;

public record Employee(int Id, string Name);

public class Program
{
    public static void Main()
    {
        Employee emp1 = new Employee(101, "John");
        Employee emp2 = new Employee(101, "John");

        Console.WriteLine(emp1 == emp2);

        Employee emp3 = emp1 with { Name = "David" };

        Console.WriteLine(emp3);
    }
}

Output:

True
Employee { Id = 101, Name = David }

Program Explanation

  1. record Employee defines a record type.

  2. Employee(int Id, string Name) declares a positional record with two properties.

  3. emp1 and emp2 contain identical property values.

  4. emp1 == emp2 returns true because records support value-based equality.

  5. The with expression creates a copy of emp1 with a modified Name.

  6. The original emp1 remains unchanged.

How Records Work

             Employee Record
                   |
         +---------+---------+
         |                   |
         v                   v
       emp1                 emp2
   Id   = 101           Id   = 101
   Name = John          Name = John
         |                   |
         +---------+---------+
                   |
                   v
            Compare Values
                   |
                   v
                  True


              emp1
         Id = 101
         Name = John
                   |
                   | with { Name = "David" }
                   v
              emp3
         Id = 101
         Name = David

Class vs Record

Feature

Class

Record

Type

Reference type

Reference type (record class)

Equality

Reference equality by default

Value-based equality

Immutability

Not automatic

Positional properties are init-only

with expression

Not supported for ordinary classes in C# 9

Supported

Best suited for

Entities and behavior

Data-centric models

Real-Time Example – Web API DTO

public record CustomerDto(
    int Id,
    string Name,
    string Email
);

Usage:

CustomerDto customer = new(
    101,
    "John",
    "john@gmail.com"
);

CustomerDto updatedCustomer =
    customer with { Email = "john@company.com" };

Records are useful for DTOs because they provide concise declarations and convenient value-based comparison.

Important: Are Records Completely Immutable?

No. Records are not automatically deeply immutable.

For example:

public record Employee
{
    public int Id { get; set; }

    public string Name { get; set; } = "";
}

This record contains mutable properties.

Also, the with expression performs a shallow copy. Referenced objects inside the record may still be shared.

Key Points

  • Introduced in C# 9.0.

  • Records provide value-based equality.

  • Positional record properties use init accessors by default.

  • Supports non-destructive mutation through with.

  • Generates useful members such as ToString(), equality methods, and deconstruction for positional records.

  • C# 9 records are reference types; record struct was introduced in C# 10.

  • Useful for DTOs, immutable models, and data transfer.

Interview Answer: Records are data-oriented reference types that provide value-based equality, concise syntax, and support for non-destructive copying using the with expression.

 

2. Init-Only Properties

What are Init-Only Properties?

C# 9.0 introduced the init accessor, which allows a property to be assigned during object initialization but prevents normal reassignment afterward.

Before C# 9.0, developers commonly used set for mutable properties or constructors for immutable properties.

The init accessor provides a convenient alternative.

Example Program

using System;

public class Employee
{
    public int Id { get; init; }

    public string Name { get; init; } = "";
}

public class Program
{
    public static void Main()
    {
        Employee emp = new Employee
        {
            Id = 101,
            Name = "John"
        };

        Console.WriteLine(emp.Id);
        Console.WriteLine(emp.Name);

        // emp.Name = "David"; // Compile-time error
    }
}

Output:

101
John

Program Explanation

  1. Id and Name use the init accessor.

  2. Their values can be assigned during object initialization.

  3. Once initialization is complete, normal code cannot modify these properties.

  4. Attempting emp.Name = "David" produces a compile-time error.

How It Works

         Create Employee
                |
                v
        Object Initializer
                |
        +-------+-------+
        |               |
        v               v
     Id = 101       Name = John
        |               |
        +-------+-------+
                |
                v
       Initialization Ends
                |
                v
       Properties cannot
       be reassigned using
       normal assignment

set vs init vs get

Property

During Initialization

After Initialization

get; set;

Can assign

Can modify

get; init;

Can assign

Cannot normally modify

get;

Constructor or initializer-defined value

Cannot normally modify

Example

public class Customer
{
    public int Id { get; set; }

    public string Name { get; init; } = "";

    public string Country { get; } = "India";
}

Usage:

Customer customer = new Customer
{
    Id = 101,
    Name = "John"
};

customer.Id = 102;       // Allowed

// customer.Name = "David"; // Error
// customer.Country = "USA"; // Error

Real-Time Example – Configuration

public class ApiSettings
{
    public string BaseUrl { get; init; } = "";

    public int Timeout { get; init; }
}

Usage:

ApiSettings settings = new ApiSettings
{
    BaseUrl = "https://api.example.com",
    Timeout = 30
};

This is useful when application code should not change configuration properties after object creation.

Key Points

  • Introduced in C# 9.0.

  • Uses init instead of set.

  • Allows assignment during object initialization and constructors.

  • Helps create immutable-style objects.

  • Does not guarantee deep immutability.

  • Useful for DTOs, configuration models, and value objects.

  • Unlike required, introduced in C# 11, init does not require callers to supply a value.

Interview Answer: Init-only properties allow values to be assigned during object initialization while preventing normal modifications after initialization, helping developers design immutable objects.

 

3. Top-Level Statements

What are Top-Level Statements?

Before C# 9.0, every C# console application required a Program class and a Main() method.

C# 9.0 introduced Top-Level Statements, allowing developers to write executable code directly without explicitly defining Program and Main().

The compiler generates the required entry point automatically.

Before C# 9.0

using System;

public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Hello World");
    }
}

C# 9.0 Example Program

using System;

Console.WriteLine("Hello World");

int a = 10;
int b = 20;

Console.WriteLine($"Sum = {a + b}");

Output:

Hello World
Sum = 30

Program Explanation

  1. No explicit Program class is required.

  2. No explicit Main() method is required.

  3. Statements are written directly in the source file.

  4. The compiler generates an entry point behind the scenes.

  5. The application executes the statements in order.

How It Works

       C# Source Code
              |
              v
    Top-Level Statements
              |
              v
        C# Compiler
              |
              v
    Generates Entry Point
              |
              v
       Application Runs
              |
              v
        Output Displayed

Real-Time Example – ASP.NET Core Web API

Modern ASP.NET Core applications commonly use top-level statements in Program.cs.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

Program Explanation

  1. CreateBuilder(args) creates the application builder.

  2. AddControllers() registers MVC controller services.

  3. Build() creates the application.

  4. MapControllers() configures controller endpoints.

  5. Run() starts the web application.

This approach is commonly called the Minimal Hosting Model, introduced with ASP.NET Core 6. It uses C# 9's top-level statement capability.

Traditional vs Top-Level Statements

Traditional Main

Top-Level Statements

Requires explicit Main()

No explicit Main()

Requires a containing class

No explicit class

More boilerplate

Less boilerplate

Explicit entry point

Compiler-generated entry point

Suitable for any application complexity

Common for modern application startup

Key Points

  • Introduced in C# 9.0.

  • Removes explicit Program and Main() boilerplate.

  • The compiler generates the entry point.

  • Only one compilation unit in an application can contain top-level statements.

  • Top-level statements must appear before namespace and type declarations in the same file.

  • Supports await and returning an exit code.

  • Commonly used in ASP.NET Core 6+ applications.

Interview Answer: Top-Level Statements simplify C# application startup by allowing executable code directly in a source file without explicitly defining a Program class or Main method.

 

4. Pattern Matching Enhancements

What are Pattern Matching Enhancements?

C# 9.0 enhanced pattern matching by introducing new patterns that make conditional expressions more readable.

The important enhancements are:

  1. Relational patterns: >, <, >=, <=

  2. Logical patterns: and, or, not

  3. Improved type patterns

These features reduce repetitive if-else and switch logic.

Example Program

using System;

public class Program
{
    public static void Main()
    {
        int age = 25;

        string category = age switch
        {
            < 18 => "Minor",

            >= 18 and < 60 => "Adult",

            >= 60 => "Senior"
        };

        Console.WriteLine(category);
    }
}

Output:

Adult

Program Explanation

  1. age switch evaluates the value of age.

  2. < 18 checks whether age is less than 18.

  3. >= 18 and < 60 checks whether age is between 18 and 59.

  4. >= 60 checks whether age is 60 or above.

  5. Since age is 25, the result is Adult.

How It Works

            Age = 25
                |
                v
           Check Age
                |
       +--------+--------+
       |        |        |
       v        v        v
      <18    18 to 59   >=60
       |        |        |
       v        v        v
     Minor    Adult    Senior
                |
                v
          Output: Adult

Relational Patterns

Relational patterns compare a value against a constant.

int score = 85;

string result = score switch
{
    >= 90 => "Excellent",
    >= 75 => "Good",
    >= 50 => "Average",
    _ => "Fail"
};

Console.WriteLine(result);

Output:

Good

Patterns are evaluated in order. Therefore, >= 90 must appear before >= 75 in this example.

Logical Patterns

Logical patterns combine other patterns.

Pattern

Meaning

Example

and

Both conditions must match

>= 18 and < 60

or

Either condition can match

1 or 2 or 3

not

Negates a pattern

not null

Example:

int status = 2;

if (status is 1 or 2 or 3)
{
    Console.WriteLine("Valid Status");
}

Output:

Valid Status

not null Pattern

string? name = "John";

if (name is not null)
{
    Console.WriteLine(name);
}

This is a convenient alternative to:

if (name != null)
{
    Console.WriteLine(name);
}

Real-Time Example – Banking Application

Suppose a banking application categorizes transaction amounts.

public static string GetTransactionCategory(decimal amount)
{
    return amount switch
    {
        <= 0 => "Invalid Amount",

        > 0 and <= 10000 => "Small Transaction",

        > 10000 and <= 100000 => "Medium Transaction",

        > 100000 => "Large Transaction"
    };
}

Usage:

Console.WriteLine(GetTransactionCategory(25000));

Output:

Medium Transaction

Before and After C# 9.0

Traditional condition:

if (age >= 18 && age < 60)
{
    Console.WriteLine("Adult");
}

C# 9.0 pattern matching:

if (age is >= 18 and < 60)
{
    Console.WriteLine("Adult");
}

Both are valid. Pattern matching is particularly useful when several cases must be handled.

Key Points

  • C# 9.0 introduced relational and logical patterns.

  • and, or, and not combine or negate patterns.

  • Relational patterns support comparisons with constants.

  • Useful in validation, categorization, and business rules.

  • Works with is expressions and switch expressions.

  • Improves readability when evaluating multiple cases.

  • Complex business logic may still be clearer with regular if statements or dedicated methods.

Interview Answer: C# 9 enhanced pattern matching with relational and logical patterns, allowing developers to write concise and expressive conditions using operators such as and, or, not, and relational comparisons.

 

5. Target-Typed new()

What is Target-Typed new()?

Before C# 9.0, object creation generally required specifying the type on both sides of an assignment.

C# 9.0 introduced Target-Typed new(), allowing developers to omit the type name during object creation when the compiler can determine the type from the surrounding context.

Before C# 9.0

Employee emp = new Employee();

C# 9.0

Employee emp = new();

Both statements create an Employee object.

Example Program

using System;

public class Employee
{
    public int Id { get; set; }

    public string Name { get; set; } = "";
}

public class Program
{
    public static void Main()
    {
        Employee emp = new()
        {
            Id = 101,
            Name = "John"
        };

        Console.WriteLine(emp.Id);
        Console.WriteLine(emp.Name);
    }
}

Output:

101
John

Program Explanation

  1. Employee emp declares a variable of type Employee.

  2. new() creates an instance using the inferred target type.

  3. The compiler understands that the target type is Employee.

  4. The object initializer assigns the property values.

  5. The program displays the employee details.

How It Works

      Employee emp = new();
              |
              v
     Compiler reads target
       type: Employee
              |
              v
       Resolves new()
              |
              v
      new Employee()
              |
              v
       Object Created

More Examples

Example 1: List

Before:

List<string> names = new List<string>();

C# 9:

List<string> names = new();

Example 2: Dictionary

Before:

Dictionary<int, string> employees =
    new Dictionary<int, string>();

C# 9:

Dictionary<int, string> employees = new();

Example 3: Constructor Parameters

public class Employee
{
    public int Id { get; }

    public string Name { get; }

    public Employee(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

Usage:

Employee emp = new(101, "John");

The compiler infers the constructor's containing type from the declared variable type.

Real-Time Example – ASP.NET Core Service

public class EmployeeService
{
    private readonly List<string> _employees = new();

    public void AddEmployee(string name)
    {
        _employees.Add(name);
    }
}

This reduces repetitive type declarations and keeps the code concise.

Target-Typed new() vs var

Target-Typed new()

var

Type is known from the target context

Type is inferred from initializer

Employee emp = new();

var emp = new Employee();

Type appears on the left

Type appears on the right

Cannot use var emp = new();

Requires an initializer with an inferable type

For example:

Employee emp1 = new();          // Valid

var emp2 = new Employee();      // Valid

// var emp3 = new();            // Invalid

Key Points

  • Introduced in C# 9.0.

  • Removes repeated type names during object creation.

  • Works when the target type is known.

  • Supports constructors with parameters and object initializers.

  • Useful for collections, services, and domain objects.

  • Does not work with var emp = new(); because no target type is available.

  • Does not change object creation behavior or performance.

Interview Answer: Target-Typed new() simplifies object creation by allowing the compiler to infer the object type from its context, reducing repetitive code.

 

C# 9.0 – Quick Interview Revision

Feature

Main Purpose

Important Syntax

Records

Value-based equality and data models

record Employee(int Id, string Name)

Init-Only Properties

Restrict modification after initialization

public int Id { get; init; }

Top-Level Statements

Remove explicit Main method boilerplate

Console.WriteLine("Hello");

Pattern Matching Enhancements

Simplify complex conditions

age is >= 18 and < 60

Target-Typed new()

Simplify object creation

Employee emp = new();

Most Important C# 9 Interview Questions

For Senior .NET Developer and Technical Lead interviews, pay particular attention to these comparisons:

Priority

Interview Question

High

What is the difference between a class and a record?

High

What is the difference between set, init, and get?

High

Are records immutable by default?

High

What is the purpose of the with expression?

High

How does value-based equality work in records?

High

What are logical and relational patterns in C# 9?

Medium

What are top-level statements, and how are they used in ASP.NET Core?

Medium

What is target-typed new()?

Medium

What is the difference between var and target-typed new()?

Important takeaway: The two most significant C# 9.0 features for enterprise application design are Records and Init-Only Properties, because they support immutable-style data models and can reduce accidental state changes. Pattern matching is also important for expressing validation and business rules clearly.

← Back to Articles