Modern C# Features – Detailed Guide with Real-Time Examples
These features are especially important in modern C# and ASP.NET Core development:
Async / Await
Pattern Matching
Nullable Reference Types
Records
Tuples
Expression-Bodied Members
Local Functions
LINQ
Generics
initPropertiesrequiredMembersImproved Pattern Matching
1. Async / Await
What is Async/Await?
async and await are used to write asynchronous code.
They are especially useful when your application needs to wait for:
Database calls
REST API calls
File operations
Azure services
External services
Network operations
Instead of blocking a thread while waiting, asynchronous programming allows the application to do other work.
Synchronous example
public string GetCustomer()
{
var customer = database.GetCustomer();
return customer;
}
If the database takes 3 seconds, the thread waits for 3 seconds.
Asynchronous example
public async Task<string> GetCustomerAsync()
{
var customer = await database.GetCustomerAsync();
return customer;
}
The application can use resources more efficiently while waiting for I/O.
Real-Time ASP.NET Core Example
Suppose we have a customer API.
[HttpGet("{id}")]
public async Task<IActionResult> GetCustomer(int id)
{
var customer = await _customerService.GetCustomerAsync(id);
if (customer == null)
return NotFound();
return Ok(customer);
}
Service:
public async Task<Customer?> GetCustomerAsync(int id)
{
return await _context.Customers
.FirstOrDefaultAsync(x => x.Id == id);
}
Repository/database call:
var customer = await _context.Customers
.FirstOrDefaultAsync(x => x.Id == id);
Why is this important?
Imagine 1,000 users calling your API.
Blocking threads while waiting for database/network operations can reduce scalability.
Async programming helps ASP.NET Core handle I/O-heavy workloads efficiently.
Task vs Task<T>
Task
Used when a method doesn't return a value.
public async Task SendEmailAsync()
{
await emailService.SendAsync();
}
Task
Used when a method returns a value.
public async Task<Customer> GetCustomerAsync()
{
return await repository.GetCustomerAsync();
}
Important interview question
Q: Does async/await create a new thread?
Not necessarily.
For I/O-bound operations, await generally allows the current thread to be released while the operation completes. When the operation finishes, execution continues.
2. Pattern Matching
Pattern matching allows you to check an object's type, value, structure, or properties more elegantly.
Traditional code:
if (customer != null)
{
if (customer.Age >= 18)
{
// Adult
}
}
Pattern matching:
if (customer is { Age: >= 18 })
{
// Adult
}
Type Pattern
object value = "Hello";
if (value is string text)
{
Console.WriteLine(text.Length);
}
Here:
value is string text
checks:
Is
valuea string?If yes, assign it to
text.
Real-Time Example
Suppose you have different payment types:
public abstract class Payment
{
}
public class CreditCardPayment : Payment
{
public decimal Amount { get; set; }
}
public class UpiPayment : Payment
{
public decimal Amount { get; set; }
}
You can process them using pattern matching:
public void ProcessPayment(Payment payment)
{
if (payment is CreditCardPayment card)
{
Console.WriteLine($"Credit Card: {card.Amount}");
}
else if (payment is UpiPayment upi)
{
Console.WriteLine($"UPI: {upi.Amount}");
}
}
3. Nullable Reference Types
Nullable reference types help prevent NullReferenceException.
Before nullable reference types:
string name = null;
The compiler doesn't necessarily warn you about this.
Modern C#:
string name = "Mahesh";
means name shouldn't be null.
If null is valid:
string? name = null;
The ? tells the compiler:
This variable is allowed to contain null.
Real-Time Example
Consider:
public Customer GetCustomer(int id)
{
return repository.GetCustomer(id);
}
What if the customer doesn't exist?
The repository may return null.
Better:
public Customer? GetCustomer(int id)
{
return repository.GetCustomer(id);
}
Then:
Customer? customer = GetCustomer(100);
if (customer != null)
{
Console.WriteLine(customer.Name);
}
Or:
Console.WriteLine(customer?.Name);
Null-Coalescing Operator
string displayName = customer?.Name ?? "Unknown Customer";
Meaning:
If
customerorNameis null, use"Unknown Customer".
Real Enterprise Scenario
API:
[HttpGet("{id}")]
public async Task<IActionResult> GetCustomer(int id)
{
Customer? customer = await _service.GetCustomerAsync(id);
if (customer is null)
return NotFound();
return Ok(customer);
}
This makes nullability explicit and improves code safety.
4. Records
Records are useful for representing data, particularly immutable data.
Traditional class:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
Record:
public record Customer(int Id, string Name);
That's much shorter.
Record Equality
This is one of the major differences.
Classes generally use reference equality unless equality is overridden.
Records provide value-based equality.
var customer1 = new Customer(1, "John");
var customer2 = new Customer(1, "John");
Console.WriteLine(customer1 == customer2);
For a record, this evaluates to:
True
because the values are equal.
Real-Time API DTO
Records are excellent for immutable request/response models.
public record CustomerResponse(
int Id,
string Name,
string Email);
Controller:
[HttpGet("{id}")]
public async Task<CustomerResponse?> GetCustomer(int id)
{
return await _service.GetCustomerAsync(id);
}
with Expression
Records support non-destructive modification.
var customer1 = new Customer(1, "John");
var customer2 = customer1 with
{
Name = "David"
};
Original object remains unchanged.
5. Tuples
Tuples allow you to return multiple values from a method without creating a separate class.
Instead of:
public class CustomerResult
{
public string Name { get; set; }
public decimal Balance { get; set; }
}
You can write:
public (string Name, decimal Balance) GetCustomerDetails()
{
return ("John", 5000);
}
Usage:
var result = GetCustomerDetails();
Console.WriteLine(result.Name);
Console.WriteLine(result.Balance);
Real-Time Banking Example
public (bool Success, decimal Balance, string Message)
Withdraw(decimal amount)
{
decimal balance = 5000;
if (amount > balance)
{
return (false, balance, "Insufficient balance");
}
balance -= amount;
return (true, balance, "Withdrawal successful");
}
Usage:
var result = Withdraw(1000);
if (result.Success)
{
Console.WriteLine(result.Balance);
}
else
{
Console.WriteLine(result.Message);
}
Tuple Deconstruction
var (success, balance, message) = Withdraw(1000);
Very useful for methods returning multiple related values.
6. Expression-Bodied Members
Expression-bodied members allow you to write short methods and properties using =>.
Traditional:
public string GetFullName()
{
return FirstName + " " + LastName;
}
Expression-bodied:
public string GetFullName() =>
FirstName + " " + LastName;
Property Example
Traditional:
public string FullName
{
get
{
return FirstName + " " + LastName;
}
}
Expression-bodied:
public string FullName =>
FirstName + " " + LastName;
Real-Time Example
public class Product
{
public decimal Price { get; set; }
public decimal Tax =>
Price * 0.18m;
public decimal FinalPrice =>
Price + Tax;
}
Usage:
var product = new Product
{
Price = 1000
};
Console.WriteLine(product.FinalPrice);
Expression-bodied members are best when the logic is short and obvious.
7. Local Functions
A local function is a method defined inside another method.
Example:
public void ProcessOrder(Order order)
{
bool IsValid()
{
return order != null &&
order.Items.Count > 0;
}
if (IsValid())
{
Console.WriteLine("Order is valid");
}
}
IsValid() is accessible only inside ProcessOrder.
Why use Local Functions?
They are useful when:
Logic is needed only by one method
You want to improve readability
You want to keep helper logic private to a particular operation
You don't want to create another class-level method
Real-Time Example
public decimal CalculateOrderTotal(Order order)
{
decimal CalculateItemTotal(OrderItem item)
{
return item.Price * item.Quantity;
}
return order.Items.Sum(CalculateItemTotal);
}
The helper function is relevant only to this calculation.
8. LINQ
LINQ = Language Integrated Query.
It allows you to query:
Collections
Arrays
Lists
Databases
XML
Objects
Without LINQ
var expensiveProducts = new List<Product>();
foreach (var product in products)
{
if (product.Price > 1000)
{
expensiveProducts.Add(product);
}
}
LINQ:
var expensiveProducts = products
.Where(p => p.Price > 1000)
.ToList();
Real-Time E-Commerce Example
Suppose:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
Find products above $1,000:
var products = db.Products
.Where(p => p.Price > 1000)
.ToList();
Sort:
var products = db.Products
.Where(p => p.Price > 1000)
.OrderByDescending(p => p.Price)
.ToList();
Select only required fields:
var products = db.Products
.Where(p => p.Price > 1000)
.Select(p => new
{
p.Name,
p.Price
})
.ToList();
Important LINQ Methods
| Method | Purpose |
|---|---|
Where() | Filtering |
Select() | Projection |
OrderBy() | Sorting |
OrderByDescending() | Reverse sorting |
First() | First element |
FirstOrDefault() | First or default |
Single() | Exactly one element |
Any() | Checks whether any exists |
All() | Checks whether all satisfy condition |
Count() | Count |
Sum() | Sum |
Average() | Average |
GroupBy() | Grouping |
Join() | Joining |
ToList() | Materialization |
LINQ with EF Core
var customers = await _context.Customers
.Where(c => c.IsActive)
.OrderBy(c => c.Name)
.ToListAsync();
EF Core can translate this LINQ expression into SQL.
9. Generics
Generics allow you to write reusable, type-safe code.
Without generics:
public class CustomerRepository
{
}
You may end up creating:
CustomerRepository
ProductRepository
OrderRepository
EmployeeRepository
Instead, create a generic repository:
public class Repository<T>
{
public void Add(T entity)
{
// Add entity
}
public T GetById(int id)
{
// Get entity
return default;
}
}
Then:
Repository<Customer> customerRepository =
new Repository<Customer>();
Repository<Product> productRepository =
new Repository<Product>();
Generic Method
public T GetValue<T>(T value)
{
return value;
}
Usage:
int number = GetValue(10);
string name = GetValue("John");
Real-Time API Result
A common enterprise approach is:
public class ApiResponse<T>
{
public bool Success { get; set; }
public string Message { get; set; }
public T? Data { get; set; }
}
Then:
ApiResponse<Customer>
or:
ApiResponse<List<Customer>>
or:
ApiResponse<Product>
This gives you reusable API response structures.
10. init Properties
init properties were introduced to make objects easier to initialize while preventing modification afterward.
Traditional:
public class Customer
{
public int Id { get; set; }
}
You can change it anytime:
customer.Id = 100;
With init:
public class Customer
{
public int Id { get; init; }
public string Name { get; init; }
}
Now:
var customer = new Customer
{
Id = 100,
Name = "John"
};
But after initialization:
customer.Id = 200;
is not allowed.
Real-Time Example
Consider an order ID.
Once an order object has been created, you don't want random code changing its identity.
public class Order
{
public int OrderId { get; init; }
public DateTime OrderDate { get; init; }
public decimal Amount { get; init; }
}
Create:
var order = new Order
{
OrderId = 1001,
OrderDate = DateTime.UtcNow,
Amount = 250
};
This makes the object safer to work with.
11. required Members
required ensures that a property must be initialized when creating an object.
Example:
public class Customer
{
public required int Id { get; set; }
public required string Name { get; set; }
public string? Email { get; set; }
}
Now:
var customer = new Customer
{
Id = 1,
Name = "John"
};
This is valid.
But:
var customer = new Customer();
will generate a compiler error because required properties weren't initialized.
Real-Time Example
Suppose an employee must always have:
Employee ID
Name
Department
public class Employee
{
public required int EmployeeId { get; init; }
public required string Name { get; init; }
public required string Department { get; init; }
public string? Email { get; init; }
}
Usage:
var employee = new Employee
{
EmployeeId = 101,
Name = "John",
Department = "IT"
};
You cannot accidentally forget required information.
12. Improved Pattern Matching
Modern C# has significantly improved pattern matching.
Important patterns include:
Property patterns
Relational patterns
Logical patterns
List patterns
Switch expressions
A. Property Pattern
Instead of:
if (customer != null &&
customer.IsActive &&
customer.Age >= 18)
{
}
You can write:
if (customer is
{
IsActive: true,
Age: >= 18
})
{
Console.WriteLine("Eligible customer");
}
B. Relational Patterns
You can directly compare values:
if (age is >= 18)
{
Console.WriteLine("Adult");
}
Multiple conditions:
if (age is >= 18 and <= 60)
{
Console.WriteLine("Working age");
}
C. Logical Patterns
and
if (salary is > 50000 and < 100000)
{
Console.WriteLine("Salary is within range");
}
or
if (status is "Pending" or "Processing")
{
Console.WriteLine("Order is being processed");
}
not
if (status is not "Cancelled")
{
Console.WriteLine("Order is active");
}
D. Switch Expression
Traditional:
string GetStatus(int status)
{
switch (status)
{
case 1:
return "Pending";
case 2:
return "Approved";
case 3:
return "Rejected";
default:
return "Unknown";
}
}
Modern:
string GetStatus(int status) =>
status switch
{
1 => "Pending",
2 => "Approved",
3 => "Rejected",
_ => "Unknown"
};
This is cleaner and easier to maintain.
E. Real-Time Banking Example
Suppose a bank transaction has different states.
public record Transaction(
decimal Amount,
string Status,
bool IsFraud);
We can determine the result:
string ProcessTransaction(Transaction transaction)
{
return transaction switch
{
{ IsFraud: true }
=> "Transaction blocked",
{ Status: "Pending", Amount: > 10000 }
=> "Manual verification required",
{ Status: "Approved" }
=> "Transaction successful",
{ Status: "Rejected" }
=> "Transaction rejected",
_
=> "Unknown transaction"
};
}
This is a very good example of modern C# pattern matching in an enterprise application.
13. Putting Multiple Features Together
Now let's combine these features into a realistic Customer Service example.
public record Customer(
int Id,
string Name,
int Age,
bool IsActive);
Generic API response:
public class ApiResponse<T>
{
public required bool Success { get; init; }
public string? Message { get; init; }
public T? Data { get; init; }
}
Service:
public async Task<ApiResponse<Customer>> GetCustomerAsync(int id)
{
Customer? customer =
await GetFromDatabaseAsync(id);
if (customer is null)
{
return new ApiResponse<Customer>
{
Success = false,
Message = "Customer not found"
};
}
if (customer is
{
IsActive: true,
Age: >= 18
})
{
return new ApiResponse<Customer>
{
Success = true,
Message = "Eligible customer",
Data = customer
};
}
return new ApiResponse<Customer>
{
Success = false,
Message = "Customer is not eligible",
Data = customer
};
}
This small example uses:
async/awaitNullable reference types
Records
Pattern matching
Property patterns
Relational patterns
requiredinitGenerics
14. How These Features Fit Together in a Real .NET Application
Think about a typical enterprise application:
Angular
↓
ASP.NET Core Web API
↓
Controller
↓
Service
↓
Repository
↓
EF Core
↓
SQL Server
Modern C# features can appear throughout the architecture:
| Feature | Typical Usage |
|---|---|
| Async/Await | API, DB, HTTP calls |
| Pattern Matching | Business rules |
| Nullable Reference Types | Null safety |
| Records | DTOs/value objects |
| Tuples | Multiple return values |
| Expression-bodied members | Simple properties/methods |
| Local Functions | Small internal helper logic |
| LINQ | Collections + EF Core |
| Generics | Repository/API response/service infrastructure |
init | Immutable object initialization |
required | Mandatory object properties |
| Improved Pattern Matching | Business rules/state processing |
15. Interview Perspective
For a Senior .NET Developer / .NET Lead, don't just memorize syntax.
Be prepared to explain why and when you use each feature.
For example:
Async/Await
"I use async/await for I/O-bound operations such as database calls, HTTP requests and file operations. It improves scalability by avoiding unnecessary thread blocking."
Records
"I use records primarily for immutable data models, DTOs and value objects where value-based equality is useful."
Nullable Reference Types
"Nullable reference types provide compile-time nullability analysis and help prevent NullReferenceException by explicitly distinguishing nullable and non-nullable references."
Generics
"Generics allow reusable and type-safe implementations. In enterprise applications I use them for repositories, API response wrappers, services and reusable infrastructure."
Pattern Matching
"Pattern matching provides a concise way to perform type, property, relational and structural checks. It is particularly useful for implementing business rules and state-based processing."
16. Most Important Features to Prioritize for Interviews
If you're preparing for a .NET Lead interview, I'd prioritize them like this:
🔴 Must Know
Async/Await
LINQ
Generics
Nullable Reference Types
Pattern Matching
Records
🟠Very Important
initrequiredTuples
Improved Pattern Matching
🟢 Easy but Useful
Expression-bodied members
Local functions
Recommended Learning Sequence
I recommend studying these in this order:
C# Fundamentals
↓
Generics
↓
LINQ
↓
Async / Await
↓
Nullable Reference Types
↓
Records
↓
Tuples
↓
Pattern Matching
↓
init / required
↓
Advanced Pattern Matching
↓
ASP.NET Core Web API
↓
EF Core
↓
Microservices
↓
Azure

