Dotnet Interview Questions Part 2
This is Part 2 of my .NET interview study guide. After conducting several technical interviews for .NET positions, I noticed clear patterns in what candidates struggle with the most. This post focuses on those high-failure topics, so if you are preparing for an interview, these are the areas where most people trip up.
Threading & Concurrency
This was by far the weakest area across all interviews. Almost every candidate had significant gaps here.
Thread vs Task
A Thread is an OS-level construct. You create a thread, and the operating system manages its lifecycle. A Task is a higher-level abstraction from the Task Parallel Library (TPL) that represents an asynchronous operation. Tasks use the ThreadPool internally, which means they reuse threads instead of creating new ones.
One important thing: Task is NOT a child of Thread. They are completely separate class hierarchies. Thread lives in System.Threading, while Task lives in System.Threading.Tasks.
1 | // Thread: OS-level, you manage the lifecycle |
The key advantage of Tasks is that they support cancellation tokens, continuations, and returning results via Task<T>. With raw threads, you need shared state and manual synchronization for all of that.
async void vs async Task
This is a classic trap. async void methods cannot be awaited, and if they throw an exception, it will crash your entire application because the exception propagates to the SynchronizationContext instead of being captured in a Task.
1 | // DANGEROUS: exception crashes the app |
The only valid use case for async void is event handlers in UI frameworks (like WPF or WinForms), because the event handler signature requires void. For everything else, always use async Task.
Lock, Monitor, Mutex and Semaphore
These are all synchronization primitives, but they serve different purposes. The lock keyword is actually syntactic sugar for Monitor.Enter and Monitor.Exit wrapped in a try/finally block.
1 | private static readonly object _lockObj = new object(); |
Here is a quick comparison:
- lock / Monitor: Works within a single process. Lightweight and fast. Use this by default.
- Mutex: Works across processes. OS-level primitive. Use when you need to coordinate between separate applications (e.g., ensuring only one instance of your app runs).
- Semaphore: Allows N concurrent accesses instead of just one.
SemaphoreSlimis the lightweight in-process version.
1 | // Mutex: cross-process synchronization |
await inside lock
This is a compiler error and many candidates do not know it. You cannot use await inside a lock block because lock requires the same thread to enter and exit the critical section, but await may resume execution on a different thread.
1 | // This will NOT compile |
Thread-safe Collections
The System.Collections.Concurrent namespace provides collections designed for multi-threaded scenarios. When multiple threads read and write to a shared collection, you should use these instead of wrapping a regular collection with locks.
ConcurrentDictionary<TKey, TValue>: Thread-safe dictionary with atomic operations likeGetOrAddandAddOrUpdate.ConcurrentQueue<T>: Thread-safe FIFO queue.ConcurrentBag<T>: Thread-safe unordered collection, optimized for scenarios where the same thread produces and consumes items.BlockingCollection<T>: Wrapper that adds bounding and blocking capabilities. Great for producer-consumer patterns.
1 | var cache = new ConcurrentDictionary<string, int>(); |
AutoResetEvent vs ManualResetEvent
Both are signaling mechanisms that allow threads to wait for a signal before proceeding. Think of AutoResetEvent as a turnstile: it lets one person through and automatically closes. ManualResetEvent is like a gate: it opens and stays open until you manually close it, letting everyone through.
1 | var autoEvent = new AutoResetEvent(false); |
The volatile keyword
The volatile keyword tells the compiler and CPU to always read the variable from main memory instead of using a cached value. It prevents instruction reordering optimizations around that variable.
However, volatile does NOT make operations atomic. For atomic operations, use the Interlocked class.
1 | private volatile bool _running = true; |
LINQ Deep Dive
LINQ is one of those topics where candidates think they know it because they use it daily, but they struggle to explain what is happening under the hood.
Deferred vs Immediate Execution
Most LINQ operators like Where, Select, OrderBy, and Take use deferred execution. This means they do not execute immediately. Instead, they build up a pipeline that only runs when you enumerate the result (e.g., with foreach, ToList(), ToArray(), Count(), or First()).
This is NOT the same as Entity Framework lazy loading. Deferred execution is about when the LINQ query runs. Lazy loading is about when EF loads related entities.
1 | var numbers = new List<int> { 1, 2, 3, 4, 5 }; |
IQueryable vs IEnumerable
This distinction has real performance implications. IEnumerable<T> processes data in memory using LINQ to Objects. IQueryable<T> builds an expression tree that gets translated to SQL and executed on the database server.
If you return IEnumerable<T> from a repository method and then filter it, the entire table is loaded into memory before filtering. If you return IQueryable<T>, the filter is pushed down to the database as a SQL WHERE clause.
1 | // BAD: loads ALL products into memory, then filters in C# |
Custom LINQ Extension Methods
LINQ methods are just extension methods on IEnumerable<T>. You can write your own using extension method syntax combined with yield return for deferred execution.
1 | public static class LinqExtensions |
The yield return keyword makes this method an iterator, which means it uses deferred execution just like the built-in LINQ methods.
Modern C# Features
Many candidates are not up to date with recent C# features. Interviewers love asking about these because they reveal whether a candidate keeps learning.
Primary Constructors (C# 12)
Primary constructors let you declare constructor parameters directly in the class or struct declaration. The parameters are available throughout the entire class body.
One important distinction: unlike records, primary constructors do NOT auto-generate public properties. The parameters are captured but not publicly exposed unless you explicitly assign them.
1 | // Primary constructor (C# 12) |
The in Parameter Modifier
The in keyword passes a parameter by reference but makes it read-only inside the method. The method cannot modify the value. It exists for performance when working with large structs, avoiding the cost of copying.
1 | // 'in' = read-only reference. The method CANNOT modify the value. |
Quick comparison:
in: Read-only reference. Caller’s value is protected.ref: Read-write reference. Method can modify the caller’s value.out: Must be assigned before the method returns. Used for multiple return values.
Covariance and Contravariance
These are about type compatibility in generic types. The out keyword on a generic type parameter means covariance (you can use a more derived type). The in keyword means contravariance (you can use a less derived type).
1 | // IEnumerable<out T> is COVARIANT |
The rule of thumb: out = output positions (return types), in = input positions (method parameters).
Boxing and Unboxing
Boxing is wrapping a value type inside an object reference, which moves the value from the stack to the heap. Unboxing is extracting the value type back. Both have performance overhead because they involve memory allocation and copying.
1 | int number = 42; |
Common places where boxing happens: non-generic collections, string.Format with value types, casting a struct to an interface.
SOLID Principles
The SOLID principles are a set of five design guidelines that help you write maintainable and flexible software. Almost every interview asks about them, and candidates often know the acronyms but struggle to explain them with practical examples.
- S — Single Responsibility: A class should have one reason to change. If your
OrderServicehandles orders, sends emails, AND writes logs, it has too many responsibilities. - O — Open/Closed: Classes should be open for extension but closed for modification. Use interfaces and inheritance to add behavior without changing existing code.
- L — Liskov Substitution: Subtypes must be substitutable for their base types without breaking the program. The classic violation is
Squareinheriting fromRectangleand breaking theSetWidth/SetHeightcontract. - I — Interface Segregation: No client should be forced to depend on methods it does not use. Split large interfaces into smaller, focused ones.
- D — Dependency Inversion: High-level modules should depend on abstractions, not on concrete implementations. This is the principle behind Dependency Injection.
For detailed examples in C# with code, check out my dedicated post: Principios de Diseno SOLID
Design Patterns
Part 1 listed all 23 GoF patterns. Here is a quick reference with links to my dedicated posts for each pattern I have covered:
Creational Patterns - Deal with object creation mechanisms.
| Pattern | Purpose | Post |
|---|---|---|
| Singleton | Ensures a class has only one instance with a global access point | See example below |
| Factory Method | Delegates object creation to subclasses | Read more |
| Abstract Factory | Creates families of related objects without specifying concrete classes | Read more |
| Builder | Constructs complex objects step by step | Read more |
Structural Patterns - Deal with object composition and relationships.
| Pattern | Purpose | Post |
|---|---|---|
| Decorator | Adds behavior to objects dynamically without modifying them | Read more |
| Facade | Provides a simplified interface to a complex subsystem | Read more |
| Composite | Treats individual objects and compositions uniformly (tree structures) | Read more |
Behavioral Patterns - Deal with communication between objects.
| Pattern | Purpose | Post |
|---|---|---|
| Observer | Notifies dependents automatically when state changes (pub/sub) | Read more |
| Strategy | Encapsulates interchangeable algorithms that can be swapped at runtime | Read more |
| State | Allows an object to change behavior when its internal state changes | Read more |
| Mediator | Reduces direct dependencies between objects. MediatR is a popular .NET library for this, commonly used with CQRS | - |
Singleton is the most commonly asked pattern in interviews. In modern .NET, you should prefer registering services with AddSingleton in the DI container. But if you need to implement it manually, use Lazy<T> for thread safety:
1 | public sealed class ConnectionManager |
Memory Management & Garbage Collector
Part 1 covered the basics of garbage collection. Let’s go deeper into the topics that interviewers expect senior candidates to know.
GC Generations
The .NET Garbage Collector uses a generational approach. Objects are divided into three generations based on their lifetime:
- Gen 0: Short-lived objects. Most objects die here. Collection is very fast.
- Gen 1: A buffer between short-lived and long-lived objects.
- Gen 2: Long-lived objects like static data, caches, and objects that survived multiple collections. Collection is expensive.
Objects that survive a collection are promoted to the next generation. The GC collects Gen 0 frequently and Gen 2 rarely.
1 | var obj = new object(); |
Finalization Queue and IDisposable
Objects that have a finalizer (~ClassName) are placed on the finalization queue, which delays their garbage collection by at least one extra generation. This is why the proper Dispose pattern calls GC.SuppressFinalize(this) — to remove the object from the finalization queue and allow it to be collected sooner.
Here is the full Dispose pattern:
1 | public class DatabaseConnection : IDisposable |
The using statement automatically calls Dispose() at the end of the scope:
1 | using var connection = new DatabaseConnection(); |
Memory Profiling Tools
When you need to investigate memory issues in .NET, these are the main tools:
- dotnet-counters: Real-time monitoring of GC, CPU, and memory counters.
- dotnet-dump: Captures and analyzes process dumps for memory leaks.
- dotnet-trace: Collects diagnostic traces for performance analysis.
- Visual Studio Diagnostic Tools: Built-in memory profiler with heap snapshots.
- BenchmarkDotNet: Measures allocations and performance of code benchmarks.
SQL Advanced Concepts
Database knowledge was a common weak point. These are the topics candidates struggled with the most.
ACID Principles
ACID defines the four guarantees that relational databases provide for transactions:
- Atomicity: All operations in a transaction either succeed together or fail together. If transferring money from account A to B, both the debit and credit must happen, or neither does.
- Consistency: A transaction takes the database from one valid state to another. All constraints, foreign keys, and rules are respected.
- Isolation: Concurrent transactions do not interfere with each other. Each transaction sees a consistent snapshot of the data.
- Durability: Once a transaction is committed, the data is permanently saved even if the system crashes.
Transaction Isolation Levels
Isolation levels control how much a transaction is isolated from other concurrent transactions. Higher isolation means fewer anomalies but more locking and lower throughput.
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | Possible | Possible | Possible |
| Read Committed (default) | Prevented | Possible | Possible |
| Repeatable Read | Prevented | Prevented | Possible |
| Serializable | Prevented | Prevented | Prevented |
- Dirty Read: Reading data that another transaction has modified but not yet committed.
- Non-Repeatable Read: Reading the same row twice and getting different values because another transaction modified it.
- Phantom Read: Running the same query twice and getting different rows because another transaction inserted or deleted rows.
Many-to-Many Relationships
In relational databases, many-to-many relationships require a junction table (also called bridge or join table). For example, a Student can enroll in many Courses, and a Course can have many Students.
1 | CREATE TABLE Students ( |
In EF Core 5+, you can configure this with skip navigations, so you do not need to explicitly create the junction entity:
1 | public class Student |
ASP.NET Core Advanced
Middleware vs Filters
Both are part of the request pipeline, but they operate at different levels.
Middleware runs on every HTTP request and works with raw HttpContext. It is configured in Program.cs and executes before the MVC framework even knows which controller to call. Use middleware for cross-cutting concerns like logging, CORS, authentication, and response compression.
Filters operate within the MVC pipeline and only run for requests that reach a controller action. They have access to MVC-specific context like ActionContext. Use filters for authorization, model validation, and action-level exception handling.
1 | // Custom Middleware |
Background Processing
When you need to run long-running tasks in the background (like processing queues, scheduled jobs, or health monitoring), use IHostedService or its simpler abstract base class BackgroundService.
1 | public class QueueProcessorService : BackgroundService |
IMemoryCache vs IDistributedCache
ASP.NET Core provides two built-in caching abstractions:
IMemoryCache stores data in the application process memory. It is fast but the cache is lost on restart and is not shared across multiple app instances.
IDistributedCache uses an external store like Redis or SQL Server. Data persists across restarts and is shared across all instances of your application.
1 | // IMemoryCache: in-process, fast, single instance |
Health Checks
ASP.NET Core has built-in support for health check endpoints. You can check your application’s dependencies (databases, external APIs, disk space) and expose a /health endpoint for load balancers and monitoring tools.
1 | // Program.cs |
Entity Framework Core Advanced
AsNoTracking and the Change Tracker
By default, EF Core tracks all queried entities in the Change Tracker. This has memory and performance overhead because EF keeps a snapshot of every entity to detect changes at SaveChanges() time.
For read-only queries, use .AsNoTracking() to skip tracking and improve performance:
1 | // Tracked: EF monitors this entity for changes (slower) |
Do NOT use AsNoTracking when you intend to update the entity, because EF will not detect the changes.
Calling Stored Procedures in EF Core
Use FromSqlInterpolated for queries that return entities and ExecuteSqlInterpolated for non-query operations like INSERT, UPDATE, or DELETE.
1 | // Query that returns entities |
Always use the interpolated versions instead of FromSqlRaw to avoid SQL injection — EF automatically parameterizes the interpolated values.
Inheritance Mapping (TPH, TPT, TPC)
EF Core supports three strategies for mapping class hierarchies to database tables:
- TPH (Table Per Hierarchy): All types in one table with a discriminator column. This is the default in EF Core. Fast queries but lots of nullable columns.
- TPT (Table Per Type): One table per class in the hierarchy, joined by foreign key. Better normalization but slower queries due to JOINs.
- TPC (Table Per Concrete type): One table per concrete class, no base table. No nullable columns, no JOINs, but data duplication for shared properties.
1 | // TPH configuration (default) |
Exception Handling
This was listed as an interview topic in Part 1 but never covered. Here are the key concepts.
Exception Hierarchy
.NET exceptions inherit from System.Exception. The two main branches are SystemException (runtime errors like NullReferenceException, InvalidOperationException) and ApplicationException (originally intended for user-defined exceptions, but Microsoft now recommends deriving directly from Exception instead).
The golden rule: catch specific exceptions, not general ones.
1 | // BAD: swallows everything, including OutOfMemoryException |
Exception Filters
C# 6 introduced exception filters using the when keyword. The filter runs before the stack unwinds, so you can inspect the exception without catching and re-throwing it.
1 | try |
Global Exception Handling in ASP.NET Core
For API applications, you should handle unhandled exceptions globally to return consistent error responses. In .NET 8+, use IExceptionHandler:
1 | public class GlobalExceptionHandler : IExceptionHandler |
Architecture & Patterns
The Outbox Pattern
The Outbox pattern solves a common problem: when you need to save data to a database AND publish a message/event, one might succeed while the other fails, leaving your system in an inconsistent state.
The solution is to store the outgoing message in an outbox table within the same database transaction as your business data. A separate background process polls the outbox and publishes the messages.
1 | // Save business data AND outbox message in the same transaction |
.NET Standard
.NET Standard is a specification (not a runtime or framework) that defines a set of APIs that all .NET implementations must support. It exists so that libraries can target multiple platforms (.NET Framework, .NET Core, Xamarin) with a single binary.
.NET Standard 2.0 is the most widely supported version and covers most APIs you would need.
With the unification of .NET starting from .NET 5, new libraries should target net6.0 or later directly instead of .NET Standard, unless they need to support .NET Framework as well.
Caching Strategies
- Cache-Aside (Lazy Loading): The application checks the cache first. On a miss, it loads from the database, stores in cache, and returns. Most common pattern.
- Write-Through: Data is written to the cache and the database simultaneously. Ensures the cache is always up to date.
- Write-Behind (Write-Back): Data is written to the cache immediately and to the database asynchronously. Faster writes but risk of data loss.
The hardest part of caching is cache invalidation — knowing when to remove or update cached data. Common approaches include time-based expiration (TTL), event-based invalidation, and versioned keys.
Testing
Testing was a universal weak spot. Most candidates knew what unit tests were but struggled with testing methodology and principles.
BDD vs TDD
TDD (Test-Driven Development) follows the Red-Green-Refactor cycle: write a failing test first, write the minimum code to pass it, then refactor.
BDD (Behavior-Driven Development) extends TDD by writing tests in business-readable language using the Given/When/Then format. BDD bridges the gap between developers and stakeholders.
1 | // TDD style (xUnit) |
In .NET, use xUnit, NUnit, or MSTest for TDD. Use SpecFlow for BDD.
FIRST Principles
These are the five qualities of good unit tests:
- Fast: Tests should run in milliseconds. Slow tests discourage developers from running them.
- Isolated/Independent: Each test should be self-contained. No test should depend on the result of another test.
- Repeatable: Running the same test multiple times should always produce the same result, regardless of environment.
- Self-validating: Tests should pass or fail without human inspection. No manual checking of output.
- Timely: Tests should be written close in time to the production code, ideally before it (TDD).
Flaky Tests
A flaky test is one that passes and fails intermittently without any code changes. Flaky tests erode trust in your test suite.
Common causes:
- Shared mutable state between tests
- Timing-dependent assertions (e.g.,
Thread.Sleepthen check) - External service dependencies (APIs, databases)
- Test order dependency
How to fix:
- Isolate test state (fresh setup for each test)
- Use deterministic clocks and fakes instead of real time
- Mock external dependencies
- Quarantine known flaky tests and fix them as a priority
Common Live Coding Patterns
Interviews often include live coding exercises. Here are the patterns candidates fail the most.
Valid Parentheses
This is a classic problem: given a string containing brackets ()[]{}, determine if the brackets are properly matched and nested. The correct approach uses a Stack, not a counter.
A counter fails on cases like )( — the counter reaches zero, but the string is clearly invalid. It also fails on mixed brackets like [(]).
1 | public static bool IsValid(string s) |
Null Handling in Modern C#
Modern C# provides several operators and patterns for safe null handling. Candidates often struggle with basic null checks in live coding, especially with value types.
1 | // Null-conditional operator: short-circuits on null |
Thread Safety in Static Methods
A common misconception is that static methods run on a single thread and are therefore always thread-safe. This is wrong. A static method is thread-safe only if it does not access shared mutable state.
1 | // THREAD-SAFE: pure function, no shared state |
Anonymous Methods vs Lambda Expressions
Anonymous methods use the delegate keyword and were introduced in C# 2.0. Lambda expressions (=>) were introduced in C# 3.0 as a more concise syntax. Lambdas also support expression trees, which is what makes LINQ to SQL possible.
1 | // Anonymous method (C# 2.0) |
In practice, lambda expressions have almost entirely replaced anonymous methods. The main conceptual difference is that lambdas can be converted to expression trees (Expression<Func<T>>), which is how EF Core translates LINQ queries into SQL.