20 C# Interview Questions Every Senior .NET Developer Should Know



If you have several years of C# experience, a senior .NET interview is rarely about remembering syntax.

Most interviewers are trying to understand something deeper:

Can you make good engineering decisions when the requirements are not obvious?

You may be asked about async and await, dependency injection, LINQ, generics, exception handling, or memory management. But the strongest candidates don't just explain what these features do.

They explain when to use them, when not to use them, and what trade-offs they introduce.

After years of working with C# and .NET, I've found that senior-level interviews tend to come back to the same concepts repeatedly.

Here are 20 questions worth preparing for.


1. What's the difference between IEnumerable<T>, IQueryable<T>, and ICollection<T>?

This is a classic question because it tests whether you understand how data is evaluated.

IEnumerable<T>

IEnumerable<T> is primarily used for iterating over data that is already available in memory.

IEnumerable<int> numbers = new[] { 1, 2, 3, 4, 5 };

var result = numbers
    .Where(x => x > 2)
    .ToList();

The filtering happens in the application process.

IQueryable<T>

IQueryable<T> is commonly used with data providers such as Entity Framework Core.

IQueryable<Customer> customers = dbContext.Customers;

var result = customers
    .Where(x => x.IsActive)
    .ToList();

The query can be translated by the provider into SQL and executed by the database.

ICollection<T>

ICollection<T> represents a collection that supports operations such as adding and removing elements.

ICollection<Customer> customers = new List<Customer>();
customers.Add(customer);

Senior-level answer

Don't simply say "IQueryable is faster."

The better answer is:

IQueryable allows a provider to translate the expression into another query language, such as SQL. That can reduce the amount of data transferred to the application. But using it carelessly can also generate inefficient queries or expose persistence concerns throughout the application.

What the interviewer is testing:
Whether you understand deferred execution, query providers, and where computation occurs.


2. What is deferred execution in LINQ?

Consider this:

var result = numbers.Where(x => x > 10);

At this point, the filtering may not have happened yet.

The query is executed when you enumerate it.

foreach (var number in result)
{
    Console.WriteLine(number);
}

Or when you materialize it:

var list = numbers
    .Where(x => x > 10)
    .ToList();

This matters because the underlying data may change between creating the query and executing it.

For example:

var query = customers.Where(x => x.IsActive);

customers.Add(new Customer { IsActive = true });

var result = query.ToList();

The result can include the newly added customer depending on the underlying collection.

Senior-level consideration

A senior developer should understand that deferred execution can be useful, but accidental multiple enumeration can cause unnecessary work.

For example:

if (query.Any())
{
    var items = query.ToList();
}

Depending on the provider, this can result in multiple executions.


3. What is the difference between async and await?

async marks a method as asynchronous.

await asynchronously waits for an operation to complete.

public async Task<Customer> GetCustomerAsync(int id)
{
    return await repository.GetCustomerAsync(id);
}

The important part is that await does not mean "create a new thread."

For I/O-bound operations such as:

  • Database calls

  • HTTP requests

  • File operations

the asynchronous model allows the current thread to be released while the operation is waiting.

A common misconception

This:

await httpClient.GetAsync(url);

doesn't mean:

"Run this on another thread."

It means:

"Suspend this asynchronous method until the operation completes without blocking the current thread."

That distinction is important in ASP.NET Core applications.


4. Why should you avoid .Result and .Wait() in asynchronous code?

Consider:

var result = service.GetDataAsync().Result;

or:

service.GetDataAsync().Wait();

These calls synchronously block the current thread.

In modern ASP.NET Core applications, this can reduce scalability and create thread-pool pressure.

Prefer:

var result = await service.GetDataAsync();

And propagate asynchronous execution through the call chain.

Senior-level answer

A good interview answer should go beyond:

"await is better."

Explain that blocking asynchronous operations can waste thread-pool threads and reduce throughput under load.


5. What is dependency injection and why is it useful?

Dependency injection means an object receives the dependencies it needs rather than constructing them itself.

Instead of:

public class OrderService
{
    private readonly SqlOrderRepository _repository =
        new SqlOrderRepository();
}

use:

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

Then register the dependency:

builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();

This provides:

  • Loose coupling

  • Easier testing

  • Replaceable implementations

  • Centralized configuration

  • Better separation of responsibilities

Senior-level consideration

Dependency injection isn't automatically good just because it is DI.

Overusing abstractions can make a codebase harder to understand.

A senior developer should know where abstraction provides real value.


6. Explain Singleton, Scoped, and Transient lifetimes.

ASP.NET Core provides three common dependency injection lifetimes.

Transient

A new instance is created each time it is requested.

services.AddTransient<IEmailService, EmailService>();

Useful for lightweight, stateless services.

Scoped

One instance is created per scope.

In a typical ASP.NET Core request, this means one instance per HTTP request.

services.AddScoped<IOrderService, OrderService>();

This is commonly used for services that depend on DbContext.

Singleton

One instance is created for the lifetime of the application.

services.AddSingleton<ICacheService, CacheService>();

Useful for genuinely application-wide, thread-safe state or services.

The important interview question

The interviewer may ask:

What happens if a singleton depends on a scoped service?

This creates a lifetime mismatch and should generally be avoided.

A singleton must not directly capture a scoped dependency.


7. What's the difference between a class and a struct?

A class is a reference type.

A struct is a value type.

Example:

public class Customer
{
    public string Name { get; set; }
}

and:

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

When you assign a class variable:

var customer1 = customer2;

both variables can refer to the same object.

For a struct:

var point1 = point2;

the value is copied.

Senior-level consideration

Don't answer:

"Use structs because they are stored on the stack."

That's an oversimplification.

Value-type storage and allocation behavior depends on context. Structs can also be boxed and can appear inside heap-allocated objects.

The better rule is to use structs when they represent small, value-like data and their semantics make sense.


8. What's the difference between ref, out, and in?

These keywords control how arguments are passed.

ref

The variable must already be initialized.

void Increment(ref int value)
{
    value++;
}

out

The method must assign a value before returning.

void GetValue(out int value)
{
    value = 10;
}

in

The argument is passed by reference but intended to be read-only.

void Print(in LargeStruct value)
{
    Console.WriteLine(value);
}

For most application-level code, these should be used only when their semantics provide a clear benefit.


9. What's the difference between ==, Equals(), and ReferenceEquals()?

This is a good question because equality can become surprisingly complicated.

a == b

uses the == operator, which can be overloaded.

a.Equals(b)

uses equality logic implemented by the type.

ReferenceEquals(a, b)

checks whether two references point to the same object.

For example:

var customer1 = new Customer { Name = "John" };
var customer2 = new Customer { Name = "John" };

They are two different object references.

So:

ReferenceEquals(customer1, customer2)

returns false.

Senior-level consideration

When implementing value-based equality, you should think about:

  • Equals

  • GetHashCode

  • ==

  • !=

  • IEquatable<T>

Incorrect equality implementations can cause subtle bugs in dictionaries, hash sets, caching, and domain models.


10. What are delegates and events?

A delegate represents a reference to a method.

public delegate void NotificationHandler(string message);

You can assign a compatible method:

NotificationHandler handler = SendNotification;

Events are built on delegates and are commonly used for publisher/subscriber patterns.

public event EventHandler? OrderCreated;

The publisher raises the event:

OrderCreated?.Invoke(this, EventArgs.Empty);

Consumers can subscribe:

orderService.OrderCreated += HandleOrderCreated;

Senior-level consideration

Events create relationships between publishers and subscribers.

If subscriptions aren't managed correctly, they can contribute to unexpected object lifetimes or memory retention.


11. What are generics and why are they important?

Generics allow you to write reusable, type-safe code.

Instead of:

public object GetValue()
{
    return 10;
}

you can write:

public T GetValue<T>(T value)
{
    return value;
}

Now:

var number = GetValue(10);
var name = GetValue("John");

Generics provide:

  • Compile-time type safety

  • Reusable code

  • Less casting

  • Better API design

  • Reduced need for boxing in many scenarios

A common example is:

List<int>
List<string>
Dictionary<string, Customer>

12. What is boxing and unboxing?

Boxing converts a value type into an object reference.

int number = 10;

object value = number;

The value is boxed.

Unboxing converts it back:

int result = (int)value;

Boxing can introduce allocations and performance overhead.

This is one reason generic collections such as:

List<int>

are preferable to non-generic collections such as older APIs that store values as object.

Senior-level consideration

Don't obsess over every boxing operation.

The important question is whether boxing occurs in a hot path where it actually affects performance.


13. How does garbage collection work in .NET?

.NET uses a garbage collector to automatically reclaim memory from objects that are no longer reachable.

The GC uses generations:

  • Generation 0

  • Generation 1

  • Generation 2

Short-lived objects generally begin in Generation 0.

Objects that survive collections can be promoted to older generations.

Why does this matter?

Creating excessive temporary objects can increase allocation pressure.

For example:

for (int i = 0; i < 1_000_000; i++)
{
    var item = new SomeObject();
}

may create significant allocation pressure depending on what happens with those objects.

Senior-level answer

Don't simply say:

"Garbage collection deletes unused objects."

A stronger answer discusses:

  • Reachability

  • Generations

  • Allocation pressure

  • Large objects

  • GC pauses

  • Object lifetime

  • Measuring before optimizing


14. What's the difference between throw and throw ex?

Consider:

catch (Exception ex)
{
    throw;
}

versus:

catch (Exception ex)
{
    throw ex;
}

Use:

throw;

when rethrowing the current exception.

It preserves the original stack trace.

Using:

throw ex;

can reset the stack trace starting point and make debugging more difficult.

Better approach

Only catch exceptions when you can do something meaningful:

try
{
    await service.ProcessAsync();
}
catch (ValidationException ex)
{
    logger.LogWarning(ex, "Validation failed");
    throw;
}

Don't wrap every method in:

try
{
    // code
}
catch
{
    // log
}

just because it feels safer.


15. What's the difference between Task, Thread, and ValueTask?

A Thread represents an operating-system thread.

A Task represents an asynchronous operation.

A ValueTask can reduce allocations in certain scenarios where operations frequently complete synchronously.

Example:

public async Task<Customer> GetCustomerAsync()
{
    return await repository.GetAsync();
}

A Task is usually the default choice.

ValueTask should not be used everywhere simply because it sounds faster.

Senior-level answer

Use ValueTask only when profiling and the API's execution characteristics justify its complexity.

A senior engineer should understand that optimization should be evidence-driven.


16. What is the difference between const, readonly, and static readonly?

const

Compile-time constant:

public const int MaxRetries = 3;

readonly

Can be assigned during declaration or in a constructor:

public readonly string ConnectionName;

public Service(string connectionName)
{
    ConnectionName = connectionName;
}

static readonly

One value belongs to the type rather than each instance:

public static readonly TimeSpan DefaultTimeout =
    TimeSpan.FromSeconds(30);

A common mistake is treating const as simply "a value that never changes."

Constants have compile-time semantics and can affect binary compatibility when shared across assemblies.


17. What's the difference between an interface and an abstract class?

An interface describes a contract.

public interface IPaymentService
{
    Task ProcessAsync();
}

An abstract class can provide both contracts and shared implementation.

public abstract class PaymentService
{
    public abstract Task ProcessAsync();

    protected void LogPayment()
    {
        // Shared behavior
    }
}

When should you use each?

Use an interface when you primarily need a capability or contract.

Use an abstract class when related types genuinely share implementation or state.

Don't create an interface for every single class automatically.

For example:

IUserService
UserService

is not necessarily valuable just because dependency injection is being used.


18. Explain SOLID principles with a practical example.

SOLID consists of five principles:

S — Single Responsibility Principle

A class should have one primary responsibility.

O — Open/Closed Principle

Software should generally be open for extension but closed for modification.

L — Liskov Substitution Principle

Derived types should be usable wherever their base abstraction is expected without breaking correctness.

I — Interface Segregation Principle

Clients shouldn't be forced to depend on methods they don't need.

D — Dependency Inversion Principle

High-level code should depend on abstractions rather than concrete implementation details.

Senior-level answer

Don't simply memorize the five definitions.

Explain how you use them to prevent:

  • Giant classes

  • Tight coupling

  • Fragile inheritance hierarchies

  • Difficult testing

  • Excessive dependencies

For example, instead of a large payment service containing every payment provider:

public interface IPaymentProcessor
{
    Task ProcessAsync(Payment payment);
}

you can create separate implementations:

StripePaymentProcessor
PayPalPaymentProcessor
BankPaymentProcessor

The architecture becomes easier to extend.


19. Composition vs inheritance — which one should you prefer?

This is one of the questions that separates experienced developers from developers who have simply memorized OOP definitions.

Inheritance creates an "is-a" relationship.

Composition creates a "has-a" relationship.

Instead of:

public class PremiumOrderService : OrderService
{
}

you might compose behavior:

public class OrderService
{
    private readonly IDiscountCalculator _discountCalculator;

    public OrderService(
        IDiscountCalculator discountCalculator)
    {
        _discountCalculator = discountCalculator;
    }
}

Now the behavior can be changed without creating a large inheritance hierarchy.

Does that mean inheritance is bad?

No.

Inheritance is appropriate when there is a genuine substitutable relationship and the base abstraction is designed for extension.

The senior-level answer is:

Prefer composition when behavior needs to vary independently or when inheritance would create tight coupling. Use inheritance when the domain relationship and substitutability are genuinely strong.


20. How would you investigate a slow ASP.NET Core API?

This is probably one of the most valuable senior-level questions.

A weak answer:

"I would optimize the code."

A strong answer starts with measurement.

I'd investigate several areas.

1. Database performance

Check:

  • SQL execution time

  • Missing indexes

  • Excessive joins

  • N+1 queries

  • Large result sets

2. External services

Measure:

  • HTTP calls

  • Timeouts

  • Retries

  • Third-party APIs

3. Application code

Look for:

  • Excessive allocations

  • Expensive LINQ

  • Synchronous blocking

  • Unnecessary serialization

  • Repeated calculations

4. Infrastructure

Check:

  • CPU

  • Memory

  • Network latency

  • Thread-pool behavior

  • Container/resource limits

5. Observability

Use:

  • Logs

  • Metrics

  • Distributed tracing

  • Profilers

  • Application performance monitoring

The key principle is:

Don't optimize what you haven't measured.

For example, if an API takes 2 seconds, don't immediately rewrite the C# code.

You may discover that 1.7 seconds are spent waiting for the database.

Changing the C# code won't solve the real problem.


Bonus: What Makes a Senior-Level C# Answer Different?

The biggest difference isn't the amount of syntax you know.

It's how you think about trade-offs.

A junior answer might be:

"Dependency injection makes code loosely coupled."

A senior answer is more like:

"Dependency injection reduces coupling and improves testability, but introducing abstractions everywhere can increase complexity. I'd introduce an abstraction where I need substitutability, isolation, or a meaningful architectural boundary."

That's the difference interviewers are often looking for.


5 Follow-Up Questions Interviewers May Ask

Once you've answered the basic question, expect the interviewer to go deeper.

For example:

After async/await

They may ask:

What happens to the thread while an I/O operation is waiting?

After dependency injection

They may ask:

Why shouldn't a singleton directly depend on a scoped service?

After LINQ

They may ask:

When does this query actually execute?

After garbage collection

They may ask:

What would you investigate if an API has high memory usage?

After SOLID

They may ask:

Can following SOLID principles too aggressively make a system worse?

These follow-up questions are where senior-level interviews often become interesting.


How I Would Prepare for a Senior C# Interview

I wouldn't try to memorize 200 questions.

Instead, I'd organize preparation around a few major areas:

C# Fundamentals

Understand:

  • Value vs reference types

  • Generics

  • Delegates

  • Events

  • Equality

  • Exceptions

  • Collections

Asynchronous Programming

Know:

  • Task

  • async

  • await

  • Cancellation

  • Exception handling

  • Concurrency

  • Thread-pool behavior

ASP.NET Core

Understand:

  • Middleware

  • Dependency injection

  • Configuration

  • Logging

  • Authentication

  • Authorization

  • API performance

Data Access

Know:

  • EF Core

  • LINQ

  • Tracking vs no-tracking

  • Transactions

  • Query performance

  • Indexes

Architecture

Be comfortable discussing:

  • SOLID

  • Clean Architecture

  • Dependency inversion

  • Composition

  • Design patterns

  • Distributed systems

Performance

Know how to investigate:

  • High CPU

  • High memory

  • Slow database queries

  • Slow APIs

  • Excessive allocations

  • Latency


Final Thought

Senior .NET interviews aren't really about proving that you remember every C# keyword.

They're about demonstrating that you can make good technical decisions.

When answering an interview question, try to move beyond:

"What is it?"

and explain:

"Why would I use it?"

Then go one step further:

"When would I NOT use it?"

That final question is often where senior-level engineering judgment becomes visible.

If you can explain the trade-offs behind your decisions, support your answers with real-world examples, and demonstrate that you've actually dealt with production problems, you're no longer answering like someone who memorized interview questions.

You're answering like a senior engineer.

you are looking for interviewer or mock interviewer please comment your mail id will reach out.


Home

Comments

Popular posts from this blog

How to Install .NET 10 SDK on Windows, Linux, and macOS

Microsoft Agent Framework Explained: A .NET Developer’s Architecture Guide

Why Is Your ASP.NET Core API Using So Much Memory? 7 Causes and Fixes