Tuesday, July 28, 2026

Modern C# Features

Modern C# Features – Detailed Guide with Real-Time Examples

These features are especially important in modern C# and ASP.NET Core development:

  1. Async / Await

  2. Pattern Matching

  3. Nullable Reference Types

  4. Records

  5. Tuples

  6. Expression-Bodied Members

  7. Local Functions

  8. LINQ

  9. Generics

  10. init Properties

  11. required Members

  12. Improved 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:

  1. Is value a string?

  2. 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 customer or Name is 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

MethodPurpose
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/await

  • Nullable reference types

  • Records

  • Pattern matching

  • Property patterns

  • Relational patterns

  • required

  • init

  • Generics


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:

FeatureTypical Usage
Async/AwaitAPI, DB, HTTP calls
Pattern MatchingBusiness rules
Nullable Reference TypesNull safety
RecordsDTOs/value objects
TuplesMultiple return values
Expression-bodied membersSimple properties/methods
Local FunctionsSmall internal helper logic
LINQCollections + EF Core
GenericsRepository/API response/service infrastructure
initImmutable object initialization
requiredMandatory object properties
Improved Pattern MatchingBusiness 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

  1. Async/Await

  2. LINQ

  3. Generics

  4. Nullable Reference Types

  5. Pattern Matching

  6. Records

🟠 Very Important

  1. init

  2. required

  3. Tuples

  4. Improved Pattern Matching

🟢 Easy but Useful

  1. Expression-bodied members

  2. 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


No comments:

Don't Copy

Protected by Copyscape Online Plagiarism Checker