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
-
record Employeedefines a record type. -
Employee(int Id, string Name)declares a positional record with two properties. -
emp1andemp2contain identical property values. -
emp1 == emp2returnstruebecause records support value-based equality. -
The
withexpression creates a copy ofemp1with a modifiedName. -
The original
emp1remains 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 ( |
|
Equality |
Reference equality by default |
Value-based equality |
|
Immutability |
Not automatic |
Positional properties are init-only |
|
|
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
initaccessors 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 structwas 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
-
IdandNameuse theinitaccessor. -
Their values can be assigned during object initialization.
-
Once initialization is complete, normal code cannot modify these properties.
-
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 |
|---|---|---|
|
|
Can assign |
Can modify |
|
|
Can assign |
Cannot normally modify |
|
|
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
initinstead ofset. -
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,initdoes 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
-
No explicit
Programclass is required. -
No explicit
Main()method is required. -
Statements are written directly in the source file.
-
The compiler generates an entry point behind the scenes.
-
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
-
CreateBuilder(args)creates the application builder. -
AddControllers()registers MVC controller services. -
Build()creates the application. -
MapControllers()configures controller endpoints. -
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 |
No explicit |
|
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
ProgramandMain()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
awaitand 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:
-
Relational patterns:
>,<,>=,<= -
Logical patterns:
and,or,not -
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
-
age switchevaluates the value ofage. -
< 18checks whether age is less than 18. -
>= 18 and < 60checks whether age is between 18 and 59. -
>= 60checks whether age is 60 or above. -
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 |
|---|---|---|
|
|
Both conditions must match |
|
|
|
Either condition can match |
|
|
|
Negates a pattern |
|
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, andnotcombine or negate patterns. -
Relational patterns support comparisons with constants.
-
Useful in validation, categorization, and business rules.
-
Works with
isexpressions and switch expressions. -
Improves readability when evaluating multiple cases.
-
Complex business logic may still be clearer with regular
ifstatements 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
-
Employee empdeclares a variable of typeEmployee. -
new()creates an instance using the inferred target type. -
The compiler understands that the target type is
Employee. -
The object initializer assigns the property values.
-
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 |
|
|---|---|
|
Type is known from the target context |
Type is inferred from initializer |
|
|
|
|
Type appears on the left |
Type appears on the right |
|
Cannot use |
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 |
|
|
Init-Only Properties |
Restrict modification after initialization |
|
|
Top-Level Statements |
Remove explicit Main method boilerplate |
|
|
Pattern Matching Enhancements |
Simplify complex conditions |
|
|
Target-Typed |
Simplify object creation |
|
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 |
|
High |
Are records immutable by default? |
|
High |
What is the purpose of the |
|
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 |
|
Medium |
What is the difference between |
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.