Preparing for Senior-Level Cand .NET Interviews

Most interview prep sites cover the same surface-level topics. They ask about the difference between interfaces and abstract classes, or they want you to explain what dependency injection is. That works for mid-level roles. For senior or staff engineer positions, the bar is different. The questions dig into memory management internals, concurrency primitives under load, and the kind of decisions that matter when your system is already in production. I spent years writing enterprise applications on .NET Framework and then migrated everything to .NET Core and beyond. The interview questions I want to talk about aren't the ones you find on Stack Overflow. They're the ones that reveal whether someone has actually shipped code at scale or just followed tutorials.

Advanced Dot Net Interview Questions That Actually Matter

Question 1: Explain how the garbage collector handles Gen 2 heaps under memory pressure and what you would do if you saw sustained high Gen 2 collection times.

Gen 2 collections are expensive because they stop the world and traverse the entire managed heap. When I was debugging a production WCF service that kept throwing OutOfMemoryExceptions every 48 hours, the GC logs showed Gen 2 collections taking 3 to 5 seconds each. The root cause was a static Dictionary<string, byte[]> used as a cache that grew without bound. Every request added entries and nothing ever removed them. The working set crept up past two gigabytes and the server started swapping on Gen 2 collections constantly. The fix wasn't adding more memory. It was replacing the Dictionary with an LRU-style cache using a ConcurrentDictionary and a background sweep task that evicted entries older than 30 minutes. Gen 2 collection time dropped from an average of 4 seconds to under 200 milliseconds within a week of deployment. The key realization was that large object heap compaction doesn't happen generically. Objects above 85,000 bytes go to the LOH and don't get moved during compacting collections unless you call GC.Collect with a specific parameter. That changed in .NET Core 3.0 where LOH compaction became configurable. If you're asking candidates this question, listen for whether they mention Gen 2 specifically, not just "the garbage collector." Anyone who only talks about short-lived objects and IDisposable hasn't dealt with long-running server processes. Also pay attention to whether they bring up server versus workload GC modes. Server GC spawns a dedicated heap per logical processor and it makes a real difference on machines with eight or more cores.

Question 2: Describe the exact execution path of an async/await method when the awaited task is already completed.

There's a common misconception that async/await always involves the thread pool. It doesn't. When the awaited task is already completed, the async state machine simply captures the current synchronization context, schedules a continuation on it, and returns immediately. No thread pool work is invoked. If you're on the main UI thread in a WPF app and you await a Task.FromResult(true), the continuation runs on the UI thread dispatcher directly. The compiler transforms your async method into a state machine struct that implements IAsyncStateMachine. The MoveNext method is what actually runs the body. Here's the thing most people skip: the state machine captures this automatically, which means every local variable in your async method becomes a field on the state machine struct. That has allocation implications. A struct-based state machine is fine until you hit reference type locals or catch blocks, which force the compiler to allocate the state machine on the heap. I've seen a hot path in a trading system where removing unnecessary locals from an async method saved roughly 12 megabytes per hour in garbage from state machine allocations alone. Here's a counter-intuitive point: ConfigureAwait(false) isn't just about avoiding deadlocks. It also skips the synchronization context capture overhead. On high-throughput web APIs, the difference is small per request but it adds up. I measured a 3 to 5 percent throughput improvement on an ASP.NET Core endpoint that was doing a lot of synchronous-to-async boundary crossing.

Question 3: What happens under the hood when you use Span<T> across an await point?

Span<T> cannot cross await points because it's a ref struct. The compiler will reject it outright. I once joined a team where a developer had written a custom buffer parsing pipeline using Span<T> for zero-copy string splitting and then tried to make the parse step asynchronous to read from a network stream. The code wouldn't compile and they were stumped. The workaround is to use IBufferWriter<T> or Stream for the async boundary and then convert to ReadOnlySpan inside the synchronous parts. You can also use MemoryPool<T> for pooled buffers that survive across async operations. The pattern looks like this: receive data into a pooled buffer, slice it into ReadOnlyMemory blocks, process them synchronously with Span operations, and discard the pool rental when done. What I find interesting is that Span still has a place in async code, just not the part that crosses the await boundary. The parsing logic itself can be extremely fast because it avoids allocations entirely. I benchmarked a JSON token reader that used Span vs one that used string.Substring and the Span version was roughly 14 times faster on a 500-megabyte log file with no allocations below the large object heap.

Get the Full Details

Dot Net Interview Questions and Answers PDF | PDF | Ajax (Programming) | World Wide Web
Dot Net Interview Questions and Answers PDF | PDF | Ajax (Programming) | World Wide Web

Question 4: How do you handle distributed locking in a .NET application when multiple instances need to coordinate on a shared resource?

The naive answer is using Redis or a database row lock. Both work but they have failure modes that matter in production. I worked on a job processing system where three worker instances shared a Redis-based lock using SET NX EX. We had a race condition where a worker would acquire the lock, process a job, and then crash before releasing it. The lock expired after the configured TTL, but during that gap another worker could see the stale lock state and attempt to process the same job. The solution involved two changes. First, we added a distributed semaphore instead of a simple lock so we could track which worker held which lock. Second, we implemented a fencing token pattern where each lock acquisition incremented a monotonically increasing counter stored in Redis. Before executing the job, the worker verified the token matched what it had when it acquired the lock. This prevented stale workers from accidentally writing results that belonged to a newer lock holder. For scenarios where you don't need distributed locking and can tolerate eventual consistency, consider using a queue-based approach instead. Put work items in a reliable queue like Azure Service Bus or RabbitMQ and let the queue enforce exactly-once or at-least-once semantics. Distributed locks are expensive and they're a single point of failure. I'd recommend them only when you genuinely need mutual exclusion across independent processes.

Question 5: Walk me through how the .NET runtime resolves interface calls at the JIT level.

Interface calls in .NET use a virtual dispatch table, but the mechanism is slightly different from virtual method dispatch on class hierarchies. Each object has a method table pointer and an interface map that the JIT generates at compile time. When you call an interface method, the runtime looks up the vtable slot based on the interface definition, not the concrete type. Here's something most people don't realize: interface calls can be inlined by the JIT under certain conditions. If the JIT can prove at compile time which concrete type implements the interface and that type hasn't changed since compilation, it will inline the call and eliminate the virtual dispatch entirely. This matters for tight loops. I optimized a numerical simulation where switching from interface-based strategy objects to a direct generic type reduced the inner loop execution time by about 18 percent because the JIT could inline all the calls instead of going through the vtable. The flip side is that generics and interfaces interact in ways that can hurt performance if you're not careful. When you constrain a generic with an interface, the JIT generates a specialized version for each concrete type. That's good for inlining but it increases code size. On a service with 200+ generic instantiations, this can push the native code cache to its limits and cause frequent cache flushes. I've seen this on containerized deployments where the GC and JIT are competing for memory.

Question 6: What are the actual tradeoffs between using Channels versus Subject in Rx.NET for backpressure handling?

System.Threading.Channels was introduced in .NET Core 3.0 and it's fundamentally different from Reactive Extensions in how it handles backpressure. A Channel with a bounded capacity will block the producer when the buffer is full. An Observable with a Subject and a large or unbounded buffer will keep accepting elements and only apply backpressure through operators like ObserveOn or throttle. The practical difference shows up when you're processing telemetry data. I built a pipeline that ingested 50,000 events per second from IoT devices. With an Rx Subject using OnNext, the buffer grew without bound during traffic spikes and we started seeing allocations in the hundreds of megabytes between collections. Switching to a BoundedChannel with waitOrTimeout semantics and a capacity of 10,000 meant producers would slow down naturally instead of consuming memory. The peak memory usage dropped from 800 megabytes to roughly 120 megabytes. Channels are simpler and they integrate better with async/await patterns. Rx subjects have a richer operator set for combining streams, subscribing to multiple sources, and handling errors with retry policies. If your problem is purely about producer-consumer coordination with backpressure, Channels is the right tool. If you need to merge, filter, and transform multiple event streams, stick with Rx. Don't use Rx for simple pipelining just because you're comfortable with it.

Dot net interview questions and asnwers | PDF
Dot net interview questions and asnwers | PDF

Question 7: When would you choose span-based memory over pinned arrays and what are the risks?

Pinned arrays prevent the garbage collector from moving an object, which is necessary for unsafe code, interop calls, and passing pointers to unmanaged functions. The risk is fragmentation. A pinned object stays in place until you explicitly unpin it, and if you pin many large objects simultaneously, the GC can't compact the managed heap effectively. I've seen servers with severe heap fragmentation where the free memory was there in small scattered blocks but new large allocations failed because no single contiguous block was available. Span<T> avoids pinning in most cases because it operates on contiguous memory within an existing allocation. When you slice a Span, you're just adjusting offset and length pointers. No pinning, no fragmentation risk. The limitation is that you can't pass a Span to a method that needs a stable pointer for an indefinite duration. For interop, you still need pinning or GCHandle. The typical pattern is to use Span for all in-process manipulation and pin only at the boundary where you cross into unmanaged code. There's also a subtle risk with Span and async. Because Span is a ref struct, you can't store it in fields or pass it across async boundaries. This is by design but it catches people off guard when they try to reuse parsing logic that was written for synchronous contexts inside an async handler. The workaround is to process the span synchronously within a short-lived method and only propagate the result forward.

Question 8: Explain the difference between IHostedService and BackgroundService and when you should use each.

IHostedService is the original interface from ASP.NET Core. It has two methods: StartAsync and StopAsync. BackgroundService is a base class introduced later that wraps IHostedService and gives you an ExecuteAsync method to override. Functionally they do the same thing, but BackgroundService handles the cancellation token propagation automatically, which means less boilerplate. The real question isn't which one to use, it's whether you should be running background work at all. I've seen teams pile on HostedServices until their application had 15 or 20 of them running concurrently. Each one holds a thread for its lifetime, and on a container with limited CPU, those threads compete with request handling threads. The application would start fine but degrade under load because the background services were consuming CPU cycles that should have gone to the HTTP pipeline. The alternative is to use a task-based approach with explicit lifetime management tied to the application shutdown token. You create tasks in your startup code and await them with a timeout during shutdown. It gives you more control and makes it obvious which tasks are critical versus which can be safely cancelled. For simple fire-and-forget work like log rotation or metric flushing, BackgroundService is fine. For anything that affects correctness or performance, consider whether it needs to run continuously or just periodically.

A few things these questions actually test

The common thread across all of these questions is whether you've dealt with the consequences of your code choices. Knowing that Gen 2 collections are slow is different from having watched your application spend 40 percent of its CPU time in GC. Understanding async/await syntactically is different from having traced a deadlock caused by synchronization context capture in a library that callers didn't write. The best candidates I've hired weren't the ones who knew every API surface. They were the ones who could describe a specific incident, what went wrong, how they diagnosed it, and what they changed. The technical depth comes from having broken things in production and fixed them. If you're preparing for these interviews, spend less time memorizing definitions and more time thinking about the systems you've actually worked on. Pick one problem you solved recently and be ready to explain it in detail. That will serve you better than any list of questions.

dot net interview questions Archives - blog.frontlinesedutech.com
dot net interview questions Archives - blog.frontlinesedutech.com