C# Interview Questions for Senior .NET Engineer - Питання та відповіді

Programming
Previous Next

🧠 Core Language & Fundamentals

  • What is the difference between class / struct / record in C#?
    When would you use them?

In C#, a class is a reference type, mutable by default, supports inheritance, and uses reference equality unless overridden.
A struct is a value type, copied when passed, and typically used for small, performance-critical data.
A record is mainly for immutable data with value-based equality, supports with expressions, deconstruction, and a generated ToString().
A record struct combines value-type semantics with value-based equality, and is mutable by default.

Usage: classes for complex or inheritable objects, structs for small values, and records for immutable data models or DTOs.

What happens if a record contains a property that is a class and you use a with expression? How does it affect equality and mutability?

  • Explain the difference between interface and abstract class.

Both interfaces and abstract classes are used to define contracts for other classes.

An interface defines a contract that a class must implement. A class can implement multiple interfaces, which makes them useful for defining capabilities across different types.

An abstract class can provide both a contract and shared implementation. It supports constructors, fields, and implemented methods. However, a class can inherit from only one abstract class.

Since C# 8, interfaces can also contain default method implementations, but they still cannot have instance state like fields.

In practice, I use interfaces for defining behavior and abstraction, and abstract classes when I need to share common logic or state between related classes.

Can a class implement two interfaces that have methods with the same signature but require different implementations? How can you call a specific implementation in that case?


  • What is boxing and unboxing? When does it happen?

Boxing is the process of converting a value type (like int or struct) into a reference type (object or interface).
Unboxing is the reverse process — extracting the value type from the object.

Boxing happens when a value type is assigned to an object or interface.
Unboxing happens when you cast that object back to the original value type.

Boxing creates a new object on the heap, so it has a performance cost. Unboxing also requires casting and can throw an exception if the types don’t match.

Why is boxing considered expensive, and how can you avoid it in high-performance code?

  • What is the difference between == and .Equals()?

The == operator and .Equals() are both used for comparison, but they behave differently.

The == operator compares references for reference types and values for value types by default, although it can be overloaded.

The .Equals() method is used for value-based comparison and can be overridden to define custom equality logic.

One important difference is that calling .Equals() on a null object will throw an exception, while == can safely compare null values.

What is the difference between object.Equals(a, b) and a.Equals(b)?

  • What are the access modifiers in C#?

In C#, there are six main access modifiers:
public, private, protected, internal, protected internal, and private protected.

public – accessible from anywhere
private – accessible only within the same class
protected – accessible within the class and its derived classes
internal – accessible within the same assembly
protected internal – accessible from the same assembly or from derived classes
private protected – accessible only within the same assembly and in derived classes

These modifiers can be applied to classes, methods, properties, fields, and other members to control visibility and encapsulation.

What is the difference between protected internal and private protected?

⚡ Memory Management & Performance

  • How does garbage collection work in .NET? What are generations in GC?

In .NET, garbage collection automatically manages memory by reclaiming objects that are no longer in use. It works in a non-deterministic way and is triggered when the runtime decides it's necessary, typically when memory pressure increases.

The GC uses a generational model with three generations: Gen 0, Gen 1, and Gen 2.

  • "Gen 0" is for short-lived objects
  • "Gen 1" is a buffer between short- and long-lived objects
  • "Gen 2" is for long-lived objects

Objects that survive collections are promoted to higher generations. This improves performance because most objects are short-lived and collected quickly.

There is also a Large Object Heap for large allocations, which is collected less frequently.

Although it's possible to force garbage collection using GC.Collect(), it is generally not recommended.

What happens when an object survives multiple garbage collections?


  • What is the Large Object Heap (LOH)?

The Large Object Heap (LOH) is a separate part of the managed heap used to store large objects, typically those larger than 85 KB.

It is not a separate generation, but it is collected together with Gen 2. LOH is optimized for large allocations, so objects there are collected less frequently to reduce overhead.

By default, the LOH is not compacted, which can lead to memory fragmentation over time.

Why can the Large Object Heap lead to memory fragmentation, and how can it be mitigated?

  • What is Span<T> and when should you use it?

Span<T> is a lightweight value type that represents a contiguous region of memory. It provides a safe and efficient way to work with slices of arrays, stack memory, or unmanaged memory without additional allocations.

It contains a reference to the memory and its length, allowing direct access without copying data.

Span<T> is mainly used for performance-critical scenarios where avoiding allocations and copying is important, such as parsing or processing large data.

However, since it is a ref struct, it is restricted to the stack and cannot be used in async methods or stored in heap objects.

Why can't Span<T> be used in async methods or stored as a class field?

  • How can you reduce memory allocations in C#?

To reduce memory allocations in C#, I focus on minimizing unnecessary object creation.

Some common techniques include:

Preallocating collections to avoid resizing
Avoiding boxing by using generics instead of object
Using StringBuilder for string concatenation and Span<T> to avoid copying data
Using structs for small, short-lived data to avoid heap allocations

In general, I try to reduce allocations in hot paths and reuse memory whenever possible.

Why can using structs sometimes increase memory usage or hurt performance instead of improving it?

  • IDisposable pattern

The Disposable Pattern is used to release unmanaged resources in a deterministic way.

It is implemented using the IDisposable interface and the Dispose() method, which allows developers to explicitly free resources when they are no longer needed.

If Dispose() is not called, the garbage collector will eventually clean up the object, but this is non-deterministic. A finalizer can be used as a fallback for unmanaged resources, but it is less efficient.

The pattern typically separates cleanup of managed and unmanaged resources, ensuring proper resource management.

Why is it recommended to avoid finalizers when possible?

🔄 Async / Await & Multithreading

  • How does async/await work under the hood?

Under the hood, async/await is implemented using a state machine generated by the compiler.

When an async method is called, it starts executing synchronously until it reaches the first await. At that point, if the awaited task is not yet completed, the method returns, and the remaining execution is scheduled as a continuation.

The compiler transforms the method into a state machine that tracks progress and resumes execution when the awaited task completes.

This allows non-blocking asynchronous code without creating new threads directly.

Does async/await create a new thread? If not, how does asynchronous code run?

  • What is the difference between TaskThreadand ValueTask?

A Thread is a low-level construct that represents an actual OS thread and is used for executing code in parallel.

A Task is a higher-level abstraction over threads. It represents an asynchronous operation and is typically executed using the thread pool, making it more efficient and easier to manage.

A ValueTask is a lightweight alternative to Task that avoids allocations when the result is already available or can be completed synchronously.

In practice, I use:

Thread for low-level control (rarely)
Task for most async operations
ValueTask in performance-critical scenarios to reduce allocations

When can using ValueTask actually make performance worse instead of better?

  • What happens if you don’t await a task?

If you don’t await a task, it will still start executing, but the calling code will continue without waiting for it to complete.

This can lead to several issues:

Exceptions may go unobserved
You lose control over execution order
The application may finish before the task completes

In some cases, this is intentional (fire-and-forget), but it should be used carefully.

What is the difference between async Task and async void, and why is async void dangerous?

  • What is a deadlock in async code? How can you avoid it? What is ConfigureAwait(false) and when should you use it?

A deadlock in async code typically happens when a thread is blocked waiting for an async operation to complete, while that async operation is trying to resume on the same thread.

This often occurs when using .Result or .Wait() on async code, especially in environments with a SynchronizationContext like UI or ASP.NET.

To avoid deadlocks:

always use await instead of blocking calls
avoid mixing synchronous and asynchronous code

ConfigureAwait(false) tells the runtime not to capture the current synchronization context. This allows the continuation to run on any thread, which helps prevent deadlocks and improves performance.

It is typically used in library code, where you don’t need to resume on the original context.

Why can using ConfigureAwait(false) break your code in UI applications?

  • Difference between Parallel.ForEach and Task.WhenAll?

Parallel.ForEach is designed for CPU-bound work and uses multiple threads to process items in parallel.

Task.WhenAll is used for asynchronous operations, typically I/O-bound, and allows multiple tasks to run concurrently without blocking threads.

Parallel.ForEach blocks the calling thread until all work is done, while Task.WhenAll is non-blocking and works with async/await.

In practice, I use:

Parallel.ForEach for CPU-intensive processing
Task.WhenAll for async operations like API calls or database requests

What happens if you use Parallel.ForEach with async code?

🧵 Threading & Concurrency

  • What is a race condition?

A race condition occurs when multiple threads access and modify shared data at the same time, and the final result depends on the timing of their execution.

This can lead to unpredictable and incorrect behavior because the operations are not properly synchronized.

To avoid race conditions, synchronization mechanisms like lock, Monitor, or other thread-safe constructs should be used.

Can race conditions occur even if your code looks correct and works most of the time?

  • What synchronization primitives do you know (lock, Monitor, SemaphoreSlim, etc.)?

In .NET, synchronization primitives are used to control access to shared resources in multithreaded code.

lock (Monitor) – the most common way to ensure only one thread enters a critical section. It is a syntactic shortcut over Monitor.Enter/Exit.
Monitor – provides more control than lock, allowing features like TryEnter, wait/pulse signaling, and timeout support.
Semaphore / SemaphoreSlim – limits the number of threads that can access a resource at the same time. SemaphoreSlim is lightweight and commonly used for async scenarios.
Mutex – similar to lock, but can be used across multiple processes. It is heavier and slower.
ReaderWriterLockSlim – allows multiple concurrent readers but exclusive writers, useful for read-heavy scenarios.

In practice, I use lock for simple cases and SemaphoreSlim for controlling concurrency in async code. 

Why should you avoid using lock in async code, and what should you use instead?

  • What is the difference between lock and Mutex?

Both lock and Mutex are used for synchronization, but they differ in scope and performance.

lock (Monitor) is a lightweight synchronization primitive used only within a single process. It is fast and commonly used for protecting critical sections in multithreaded code.
Mutex is a heavier synchronization primitive that can work across multiple processes. It ensures only one thread (even from different applications) can access a resource at a time.

Because of this, lock is much faster and preferred for in-process synchronization, while Mutex is used when cross-process synchronization is required.

Why is Mutex significantly slower than lock?

  • What are concurrent collections?

Concurrent collections in .NET are thread-safe data structures designed for use in multithreaded scenarios without requiring explicit locking.

They handle synchronization internally, which helps avoid race conditions and simplifies code.

Some common examples include:

ConcurrentDictionary
ConcurrentQueue
ConcurrentStack
BlockingCollection

These collections are optimized for concurrent access and often use fine-grained locking or lock-free algorithms for better performance.

In practice, I use them when multiple threads need to safely read and write shared data without manually managing locks.

Why can concurrent collections still lead to race conditions in some scenarios?

🔗 LINQ & Collections

  • How does LINQ work internally?

LINQ works by translating queries into method calls using extension methods like Where, Select, etc.

For in-memory collections (IEnumerable), LINQ uses deferred execution, meaning the query is not executed immediately but only when you iterate over the results.

Each LINQ operator typically returns an iterator, and the execution is performed step-by-step using yield and enumerators.

For external data sources (like databases with IQueryable), LINQ builds an expression tree, which is then translated into another query language, such as SQL.

  • What is deferred execution?

Deferred execution means that a query is not executed when it is defined, but only when the results are actually iterated over.

In LINQ, methods like Where and Select use deferred execution, so the data is processed only when needed.

Immediate execution means the query is executed right away, and the results are materialized in memory. Examples include methods like ToList(), ToArray(), or Count().

The key difference is that deferred execution improves performance and flexibility, while immediate execution forces evaluation and stores the results.

What problems can occur with deferred execution?

  • Difference between IEnumerable and IQueryable and IAsyncEnumerable?

IEnumerable is used for in-memory iteration. LINQ operations are executed in the application using deferred execution.

IQueryable is used for querying external data sources like databases. It builds expression trees that are translated into another query language, such as SQL, and executed remotely.

IAsyncEnumerable is used for asynchronous iteration. It allows consuming data with await foreach, which is useful for streaming data or working with I/O-bound operations.

In practice:

IEnumerable → in-memory collections
IQueryable → database queries
IAsyncEnumerable → async data streaming

What happens if you call ToList() on an IQueryable?

  • What are the performance implications of LINQ?

LINQ improves readability and developer productivity, but it can have performance costs if used incorrectly.

Some key implications:

Deferred execution can lead to multiple enumerations and repeated work
LINQ may introduce extra allocations (iterators, delegates)
It can be slower than manual loops in performance-critical code
With IQueryable, inefficient queries can be generated and executed on the database side

In practice, LINQ is great for most scenarios, but in hot paths or performance-critical code, I prefer more optimized approaches.

Why can multiple calls to Where() and Select() sometimes be inefficient?

  • Difference between Select and SelectMany?

Select projects each element into a new form, returning one result per input element.

SelectMany projects each element into a collection and then flattens the results into a single sequence.

So:

Select → one-to-one transformation
SelectMany → one-to-many + flattening

  • What is the difference between ArrayList and List<T>?

ArrayList is a non-generic collection that stores elements as object, while List<T> is a generic, strongly-typed collection.

Because of that:

ArrayList requires boxing/unboxing for value types
List<T> avoids boxing and provides better performance and type safety

List<T> also enables compile-time checks, making it safer and more efficient.

In practice, ArrayList is considered legacy, and List<T> should always be preferred.

🧩 Delegates, Events & Functional Features

  • What is a delegate?
  • Difference between Action, Func, and Predicate?
  • What are events and how are they used?
  • What are lambda expressions?
  • What are closures in C#?

🏗️ OOP & Design

  • What are SOLID principles? (briefly explain each)

SOLID is a set of five design principles that help create maintainable and scalable software:

S – Single Responsibility Principle
A class should have only one reason to change, meaning it should have a single responsibility.
O – Open/Closed Principle
Software entities should be open for extension but closed for modification.
L – Liskov Substitution Principle
Derived classes should be replaceable for their base classes without breaking behavior.
I – Interface Segregation Principle
Clients should not be forced to depend on interfaces they do not use.
D – Dependency Inversion Principle
High-level modules should depend on abstractions, not on concrete implementations.

Can you give an example where following SOLID too strictly can make the code worse

  • What is dependency injection?

Dependency Injection (DI) is a design pattern where dependencies are provided to a class from the outside instead of being created inside it.

This helps to reduce coupling, improve testability, and make the code more flexible.

There are three common types:

Constructor injection (most common)
Method injection
Property injection

In .NET, DI is typically managed by a built-in container that handles object creation and lifetime management.

What problem does dependency injection solve compared to using new inside a class?

  • What is inversion of control?

Inversion of Control (IoC) is a design principle where the control of object creation and flow is delegated to an external framework or container, instead of being handled by the application code itself.

Instead of creating dependencies directly, the application receives them from the outside.

Dependency Injection is a common way to implement IoC.

What is the difference between Inversion of Control and Dependency Injection?

📦 Advanced Language Features

  • What are yield return and iterators?

yield return is used to implement iterators in C#.

It allows a method to return elements one at a time without creating a full collection in memory. The compiler transforms such a method into a state machine that implements IEnumerable or IEnumerator.

This enables lazy (deferred) execution, where values are generated only when iterated.

It is useful for working with large datasets or streaming data efficiently.

What happens if you iterate over a yield return sequence multiple times?

  • What are extension methods?

Extension methods in C# allow you to add new methods to existing types without modifying their source code or creating a new derived type.

They are defined as static methods in static classes, with the this keyword in the first parameter to specify the type being extended.

Extension methods are widely used in LINQ and help improve code readability and reusability.

Can extension methods override existing methods of a class?

  • What is pattern matching in C#?

Pattern matching in C# is a feature that allows you to check a value against a pattern and extract data from it in a concise and readable way.

It is commonly used with is, switch, and switch expressions.

Examples include:

type patterns (x is string s)
property patterns
positional patterns
relational patterns

Pattern matching helps reduce boilerplate code and makes conditional logic more expressive.

What is the difference between a switch statement and a switch expression in C#?

  • What are nullable reference types?

Nullable reference types are a C# feature that helps detect and prevent null-related errors at compile time.

When enabled, reference types are non-nullable by default, and you must explicitly mark nullable ones using ?.

For example:

string → cannot be null
string? → can be null

The compiler provides warnings when you might assign or use null incorrectly, improving code safety and reducing runtime NullReferenceException.

Does enabling nullable reference types change runtime behavior?

  • What is reflection and when would you use it?

Reflection in C# allows you to inspect and interact with types, methods, and properties at runtime.

It can be used to:

discover metadata about types
dynamically create objects
invoke methods or access properties

It is commonly used in frameworks, dependency injection containers, serialization, and testing tools.

However, reflection has a performance cost and should be used carefully.

Why is reflection slower than normal code execution?

🔍 Exception Handling

  • What is the difference between throw and throw ex?

The difference between throw and throw ex is how they handle the exception stack trace.

throw rethrows the exception while preserving the original stack trace
throw ex resets the stack trace, making it harder to trace where the exception originally occurred

Because of this, throw is preferred when rethrowing exceptions.

  • When should you use custom exceptions?

Custom exceptions should be used when you need to represent domain-specific errors that are meaningful in your application.

They help make error handling clearer and more expressive, especially when you want to:

distinguish between different failure scenarios
provide more context about the error
handle specific cases differently in higher layers

In general, I create custom exceptions when built-in exceptions are not descriptive enough.

When should you avoid creating custom exceptions?

  • What are best practices for exception handling?

Best practices for exception handling in C# focus on clarity, correctness, and performance:

Catch only exceptions you can handle
Avoid using exceptions for normal control flow
Always preserve the stack trace (use throw, not throw ex)
Use specific exception types instead of generic ones
Add meaningful context when rethrowing exceptions
Ensure proper resource cleanup (e.g., using or finally)

In general, exceptions should be used for truly exceptional situations, not for regular logic.

Why is catching Exception considered a bad practice?

Самостоятельный отпуск Опыт заказа вывоза мусора в Киеве Магія зміни: Від ночі до дня
Магія Вечірнього Неба: Відлякуйте втомленість дня і зануртеся у світ загадок і краси Якби Росія була людиною, то як би її описав психіатр?