Types • LINQ • Async/Await • Memory & GC • Generics • 2026

C# Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 38 min read

This page is for anyone facing a C# round, from a first developer job to a senior role. Most C# interviews open with value and reference types, boxing and the choice between class, struct and record, then move to interfaces, delegates and events. The middle of the round is usually LINQ and async/await, where deferred execution and deadlocks trip people up. Stronger rounds add IDisposable, garbage collection, generics and nullable reference types, plus a production story and a judgement call. Each question shows what the interviewer is really checking, the shape of a good answer and a short answer to say out loud. Practise them, then swap in your own stories.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Types & Memory 3 questions

Easy Technical round Fresher, Mid-level Practice question

1. What's the difference between a value type and a reference type in C#? What happens when you assign one variable to another?

What the interviewer is really testing:
Whether you can predict what a copy does to the original, which is behind a lot of everyday bugs, and whether you avoid the 'value types always live on the stack' myth.
Answer frame:

Value types: structs, enums and the built-in numbers and bool; the variable holds the data itself.

Reference types: classes, strings, arrays, delegates; the variable holds a reference to an object on the heap.

Assignment: copying a value type copies the data; copying a reference type copies the reference, so both point at one object.

Sample spoken answer:

"A value type, like int, bool, a struct or an enum, holds its data directly in the variable. A reference type, like a class, an array or a string, holds a reference to an object that lives on the managed heap. The difference shows up on assignment. If I copy a struct into another variable and change the copy, the original stays the same, because I got a full copy of the data. If I copy a class reference and change a property through the copy, the original sees the change, because both variables point at the same object. The same rule applies when I pass them to a method. One thing I'd correct is the idea that value types always live on the stack. A local int usually does, but an int field inside a class lives on the heap with that object. Where it lives depends on where it's declared, not just on its type."

Code:
struct Point { public int X; }
class Box { public int X; }

var p1 = new Point { X = 1 };
var p2 = p1;      // full copy of the data
p2.X = 99;        // p1.X is still 1

var b1 = new Box { X = 1 };
var b2 = b1;      // copy of the reference
b2.X = 99;        // b1.X is now 99 too
Red flag to avoid:

Saying value types always live on the stack and reference types on the heap, with no mention that a struct field inside a class lives on the heap.

They may ask next:
  • Is string a value type or a reference type, and why does it behave like a value in comparisons?
  • What happens if you pass a struct to a method and the method changes one of its fields?
  • When would you choose to make your own type a struct?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

2. What is boxing and unboxing, and where does boxing sneak into code without you noticing?

What the interviewer is really testing:
Whether you know boxing costs a heap allocation and can spot the hidden cases, not just recite the definition.
Answer frame:

Boxing: a value type is converted to object or an interface; the runtime allocates a heap object and copies the value in.

Unboxing: an explicit cast back that needs the exact type, or it throws InvalidCastException.

Hidden cases: non-generic collections like ArrayList, storing a struct in an interface variable, APIs that take object.

Sample spoken answer:

"Boxing is what happens when a value type gets treated as an object or as an interface it implements. The runtime allocates a small object on the heap and copies the value into it. Unboxing is the explicit cast back, and it's strict: if I boxed an int, I have to unbox it as an int. Casting straight to long throws an InvalidCastException, even though int to long is normally fine. The cost is an allocation each time, plus garbage collection work later, and the boxed copy is separate from the original, so changing one doesn't change the other. It sneaks in through old non-generic collections like ArrayList or Hashtable, through methods that take a parameter of type object, and when I store a struct in an interface-typed variable. The main fix is generics: a List of int stores the ints directly with no boxing at all."

Code:
int n = 42;
object boxed = n;            // box: new heap object holding 42
int back = (int)boxed;       // unbox: exact type needed
// long bad = (long)boxed;   // throws InvalidCastException
long ok = (long)(int)boxed;  // unbox first, then convert

var old = new System.Collections.ArrayList();
old.Add(n);                  // boxes on every Add
var list = new List<int> { n }; // no boxing
Red flag to avoid:

Thinking unboxing can convert to any compatible numeric type, or not knowing that boxing allocates.

They may ask next:
  • Why does a List of int avoid boxing when an ArrayList doesn't?
  • If you box a struct and then change the original, what does the boxed copy contain?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level, Senior Practice question

3. When would you pick a class, a struct or a record for a new type? Walk me through how you decide.

What the interviewer is really testing:
Whether you understand copy semantics and equality well enough to choose deliberately, rather than defaulting to class every time or picking struct for speed without thinking.
Answer frame:

Class: reference type, identity equality by default, inheritance; the default for objects with behaviour and a lifetime.

Struct: value type copied on assignment, no inheritance; for small, short-lived values, ideally immutable.

Record: compiler-generated value-based equality, ToString and with-expressions; a record class by default, a record struct if asked.

Sample spoken answer:

"My default is a class. It's a reference type, it supports inheritance, and two instances are equal only if they're the same object, which suits things like services or entities with an identity. I'd reach for a struct when the type is small, represents a single value, like a coordinate or a money amount, and is created in large numbers where avoiding heap allocations helps. I'd keep it immutable, because a mutable struct gets copied silently and people end up changing a copy by mistake. A record is for data where two instances with the same values should count as equal, like a DTO or a message. The compiler writes Equals, GetHashCode and ToString for me, and I get with-expressions to make a changed copy. A plain record is still a reference type. If I want value semantics and value equality together, I can write record struct."

Code:
public record Money(decimal Amount, string Currency);

var a = new Money(10m, "EUR");
var b = new Money(10m, "EUR");
bool same = a == b;                  // true: value-based equality
bool sameObject = ReferenceEquals(a, b); // false: two objects
var c = a with { Amount = 20m };     // copy with one change
Red flag to avoid:

Saying structs are always faster, or that records are value types, without mentioning copying cost or that a plain record is a class.

They may ask next:
  • What goes wrong when you mutate a struct that you got back from a List of structs?
  • Two record instances have the same property values. Are they equal, and is ReferenceEquals true?
  • Why can a large struct be slower than a class?
Say it in 60 seconds

Classes & OOP 3 questions

Medium Technical round Fresher, Mid-level Practice question

4. C# interfaces can now have default method bodies. So when do you still reach for an abstract class instead of an interface?

What the interviewer is really testing:
Whether you know what each one can actually hold and choose by design intent, not by a rule learned before default interface methods existed.
Answer frame:

Interface: a contract; a class can implement many; no instance fields or constructors, but default and static members are allowed.

Abstract class: single inheritance, but can hold state, constructors and protected members, and share real implementation.

How I choose: a capability many unrelated types can have goes in an interface; a family sharing state and setup code gets a base class.

Sample spoken answer:

"An interface describes what a type can do, and a class can implement as many as it likes. Since C# 8 an interface can carry a default method body, which mostly helps library authors add a method without breaking every existing implementer. But an interface still can't have instance fields or a constructor, so it can't hold shared state. An abstract class can. It can have fields, a constructor that enforces setup, protected helpers for subclasses, and a mix of abstract and concrete members. The catch is that a class can only inherit from one base class. So I use an interface for a capability that unrelated types can share, like IComparable or a repository contract that I want to mock in tests. I use an abstract class when I have a real family of types that share state and setup logic, like a base payment processor that handles logging and retries and lets each subclass implement one step."

Red flag to avoid:

Saying interfaces can't contain any implementation in modern C#, or choosing an abstract class just to share one helper method.

They may ask next:
  • If a class implements an interface with a default method, can you call that method through a variable of the class type?
  • Why do interfaces make unit testing easier?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

5. What's the difference between overriding a method with override and hiding it with new? Which method runs in each case?

What the interviewer is really testing:
Whether you understand that override is decided by the object's runtime type and new by the variable's declared type, which is a classic source of confusing bugs.
Answer frame:

override: replaces a virtual or abstract method; the call is decided at runtime by the actual object.

new: hides a base method with an unrelated one; the call is decided at compile time by the variable's type.

Advice: new is rarely what you want; the compiler warns when you hide a method by accident.

Sample spoken answer:

"With override, the base method has to be marked virtual or abstract, and my derived method replaces it. The call is dispatched at runtime, so if I hold a Dog in a variable of type Animal and call Speak, I get the Dog's version. With new, I'm not replacing anything. I'm declaring a separate method that happens to have the same name, and it hides the base one. Which one runs then depends on the variable's declared type. Call it through an Animal variable and you get the Animal version, even though the object is a Dog. That's why new is almost never what you want. It shows up when a base class I don't control adds a method with the same name as one of mine. If I forget both keywords, the compiler treats it as hiding and gives me a warning, which I'd treat as a bug to fix. I can also mark an override as sealed to stop further overriding."

Code:
class Animal
{
    public virtual string Speak() => "...";
    public string Kind() => "Animal";
}
class Dog : Animal
{
    public override string Speak() => "Woof";
    public new string Kind() => "Dog";
}

Animal a = new Dog();
Console.WriteLine(a.Speak()); // Woof   (runtime type decides)
Console.WriteLine(a.Kind());  // Animal (variable type decides)
Console.WriteLine(((Dog)a).Kind()); // Dog
Red flag to avoid:

Saying new and override do the same thing, or not being able to say which version runs through a base-type variable.

They may ask next:
  • Can you override a method that isn't marked virtual in the base class?
  • What does sealed do on a method, and why would you use it?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

6. How do extension methods work, and what rules and limits do they have?

What the interviewer is really testing:
Whether you know extension methods are compile-time sugar over a static call, which explains their limits and why LINQ reads the way it does.
Answer frame:

Shape: a static method in a static, non-generic class, with this before the first parameter.

Resolution: the compiler rewrites the call to a static call; a real instance method with the same signature always wins.

Limits: no access to private or protected members, no new state, and the namespace must be in scope.

Sample spoken answer:

"An extension method lets me call a static method as if it were an instance method on a type I don't own. I write it as a static method inside a static class, and put the keyword this before the first parameter. So if I write IsBlank on string, I can call name.IsBlank(). Under the hood it's just syntax. The compiler rewrites that call to StringExtensions.IsBlank(name). That explains the limits. It can't reach private or protected members, it can't add fields or state, and if the type already has an instance method with the same signature, the instance method wins. The namespace also has to be imported, or the method won't show up. Because it's really a static call, it can even be called on a null reference without an exception on the call itself. LINQ is the biggest example: Where and Select are extension methods on IEnumerable."

Code:
public static class StringExtensions
{
    public static bool IsBlank(this string? value) =>
        string.IsNullOrWhiteSpace(value);
}

// usage
string? name = null;
bool blank = name.IsBlank(); // true, no exception
Red flag to avoid:

Thinking an extension method actually changes the original type or can reach its private members.

They may ask next:
  • Can you call an extension method on a null reference? What happens?
  • When would an extension method be a bad design choice compared to a normal helper or a new class?
Say it in 60 seconds

Delegates & Events 2 questions

Easy Technical round Fresher, Mid-level Practice question

7. What is a delegate in C#, and how do Func and Action relate to it?

What the interviewer is really testing:
Whether you understand delegates as type-safe references to methods, which is the base for lambdas, LINQ, callbacks and events.
Answer frame:

Delegate: a type that describes a method signature; an instance holds a reference to a matching method.

Func and Action: built-in generic delegates; Func returns a value (last type argument), Action returns void.

Multicast: delegates can combine several methods with +=; all run in order, and a return value comes from the last one.

Sample spoken answer:

"A delegate is a type-safe reference to a method. I declare a delegate type with a signature, and any method with a matching signature can be stored in a variable of that type and called later. That's how I pass behaviour around: a callback, a rule to apply, a sort comparison. In practice I rarely declare my own delegate types now, because the framework gives me generic ones. Action is for methods that return void, with up to sixteen parameters. Func is for methods that return something, and the last type argument is the return type, so a Func of int and bool takes an int and returns a bool. Lambdas are just a short way to create a delegate instance. Delegates are also multicast: I can combine several methods with plus-equals, and invoking it calls each in order. If they return values, I only get the last one, which is why multicast is really used for void methods like event handlers."

Code:
Func<int, bool> isEven = n => n % 2 == 0;
Action<string> log = msg => Console.WriteLine(msg);

log += msg => File.AppendAllText("app.log", msg + "\n");
log("started");            // runs both, in order

bool r = isEven(4);        // true
Red flag to avoid:

Describing a delegate as a function pointer with no type safety, or mixing up which of Func and Action returns a value.

They may ask next:
  • What does a lambda capture when it uses a local variable from the enclosing method?
  • If a multicast delegate has three methods and the second throws, does the third run?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

8. Why does C# have an event keyword if a public delegate field would work? And how can events cause memory leaks?

What the interviewer is really testing:
Whether you understand the encapsulation the event keyword adds, and the lifetime trap where a long-lived publisher keeps short-lived subscribers alive.
Answer frame:

Encapsulation: outside code can only subscribe and unsubscribe; only the owning class can raise or reset the event.

Raising safely: use the null-conditional invoke so a missing subscriber list isn't a crash.

Leaks: the publisher holds a reference to each subscriber; if the publisher lives longer, subscribers never get collected until they unsubscribe.

Sample spoken answer:

"An event is a delegate with guard rails. If I expose a public delegate field, any caller can invoke it, or assign it with a single equals and wipe out everyone else's handlers. With the event keyword, outside code can only use plus-equals and minus-equals. Only the class that declares the event can raise it or clear it. To raise it, I use the question-mark dot Invoke pattern, which reads the delegate once, so it won't crash if nobody subscribed. The leak comes from the direction of the reference. When a subscriber adds a handler, the publisher's delegate now holds a reference to the subscriber. If the publisher is long-lived, say a static class or a singleton, and the subscriber is something short-lived like a window or a per-request object, the subscriber can never be collected while it's still subscribed. The fix is to unsubscribe, usually in Dispose, or to design the lifetimes so the publisher doesn't outlive its subscribers."

Code:
public class PriceFeed
{
    public event EventHandler<decimal>? PriceChanged;

    public void Publish(decimal price) =>
        PriceChanged?.Invoke(this, price); // only this class can raise it
}

// outside code:
feed.PriceChanged += OnPrice;   // allowed
feed.PriceChanged -= OnPrice;   // allowed
// feed.PriceChanged = null;    // compile error
// feed.PriceChanged(this, 1m); // compile error
Red flag to avoid:

Not knowing the event keyword restricts outside callers, or saying the subscriber holds the reference that causes the leak.

They may ask next:
  • Where would you put the unsubscribe call for an object that subscribes to a static event?
  • Is raising an event with the null-conditional operator thread-safe?
Say it in 60 seconds

LINQ 4 questions

Medium Coding round Fresher, Mid-level Practice question

9. What does deferred execution mean in LINQ? Show me a case where it gives a result you might not expect.

What the interviewer is really testing:
Whether you know a LINQ query is a recipe that runs when enumerated, which explains stale results, repeated work and surprise database calls.
Answer frame:

Deferred: Where, Select and OrderBy build a query; nothing runs until something enumerates it.

Immediate: ToList, ToArray, Count, First, Sum and similar run the query right away.

Consequences: later changes to the source show up, and each enumeration runs the whole query again.

Sample spoken answer:

"Most LINQ operators like Where and Select don't do any work when you call them. They return an object that describes the query, and the real work happens when something enumerates it, like a foreach, or an operator such as ToList, Count or First. So in this example I build a query for numbers greater than one, then add another number to the list, then call Count. I get three, not two, because the filter runs at Count time and sees the new item. The other side effect is repeated work. If I enumerate the same query twice, it runs twice. With an in-memory list that's just wasted CPU, but if the source is a database query or a method with side effects, it means two round trips or doing the side effect twice. So when I need a stable snapshot, or I'll use the result more than once, I call ToList once and reuse that."

Code:
var numbers = new List<int> { 1, 2, 3 };
var big = numbers.Where(n => n > 1);  // nothing runs yet

numbers.Add(4);
Console.WriteLine(big.Count());       // 3 -> runs now, sees the 4

var snapshot = numbers.Where(n => n > 1).ToList(); // runs once
numbers.Add(5);
Console.WriteLine(snapshot.Count);    // still 3
Red flag to avoid:

Thinking the filter runs on the line where Where is called, or calling ToList everywhere without knowing why.

They may ask next:
  • Which LINQ operators force the query to run straight away?
  • Why would a code analyser warn you about possible multiple enumeration of an IEnumerable?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

10. What's the practical difference between IEnumerable and IQueryable when you query a database through an ORM?

What the interviewer is really testing:
Whether you know where the filtering actually runs, because a wrong type can pull a whole table into memory without any error.
Answer frame:

IEnumerable: LINQ to Objects; lambdas are compiled delegates that run in memory on items already loaded.

IQueryable: lambdas become expression trees that a provider translates, for example into SQL, so filtering runs in the database.

Trap: once a query is treated as IEnumerable, every later operator runs in memory after all rows are fetched.

Sample spoken answer:

"Both let me write the same Where and Select, but they run in very different places. With IEnumerable, the lambdas are compiled delegates, and the filtering happens in my process on objects that are already loaded. With IQueryable, the lambdas are kept as expression trees, and the provider, such as an ORM, translates the whole chain into SQL, so the database does the filtering, sorting and paging. The bug I've seen is a repository method that returns IEnumerable, or a call to AsEnumerable in the middle of a chain. Everything before that point goes to the database, but the Where added afterwards runs in memory, after every row has come back. It still gives the right answer, which is why nobody notices until the table gets big. The other catch with IQueryable is that the provider can only translate what it understands, so a call to my own C# method inside the lambda may fail to translate."

Red flag to avoid:

Saying they're interchangeable, or not realising an IEnumerable filter on a database query runs after all rows are loaded.

They may ask next:
  • Why can't a provider translate a call to your own helper method inside a Where clause?
  • Where in your layers would you stop exposing IQueryable, and why?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

11. You have a list of orders with a customer name and an amount. Write LINQ that returns the three customers who spent the most, with their totals.

What the interviewer is really testing:
Whether you can use GroupBy, aggregate inside groups and sort the groups, and whether you pick a sensible type for money.
Answer frame:

Group: GroupBy on the customer; each group has a Key and its orders.

Aggregate: project each group to the name, the sum of amounts and maybe a count.

Rank: OrderByDescending on the total, Take three, then materialise with ToList.

Sample spoken answer:

"I'd group the orders by customer, so each group has the customer name as its key and all their orders inside. Then I project each group into a small result with the key, the sum of the amounts and the order count. After that I sort those results by total, highest first, take three, and call ToList so the query runs once and I have a fixed result. I'd store the amount as decimal, not double, because it's money and I don't want rounding errors in binary floating point. If the orders came from a database through an ORM, the same query would usually translate into a GROUP BY with an ORDER BY and a row limit. One edge case I'd mention is ties: if two customers have the same total at third place, Take just cuts at three, so if the business cares, I'd add a second sort key like the name to make the result stable."

Code:
public record Order(string Customer, decimal Amount);

var topThree = orders
    .GroupBy(o => o.Customer)
    .Select(g => new
    {
        Customer = g.Key,
        Total = g.Sum(o => o.Amount),
        Orders = g.Count()
    })
    .OrderByDescending(x => x.Total)
    .ThenBy(x => x.Customer)
    .Take(3)
    .ToList();
Red flag to avoid:

Sorting the orders before grouping, or looping and building a dictionary by hand when the interviewer asked for LINQ.

They may ask next:
  • How would you write the same thing in query syntax instead of method syntax?
  • Why decimal and not double for the amount?
  • How would you also include each customer's largest single order?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

12. What does yield return do, and what surprise can it cause with argument checks?

What the interviewer is really testing:
Whether you know iterator methods are lazy state machines, which affects when code runs and when exceptions appear.
Answer frame:

Iterator: a method with yield return is compiled into a state machine that produces one item per MoveNext.

Lazy: no code in the body runs until enumeration starts, and it pauses after each item.

Surprise: argument checks in the body run late; split into a normal method that checks and a local iterator function.

Sample spoken answer:

"When a method uses yield return, the compiler turns it into a state machine that implements IEnumerable. Calling the method doesn't run the body. It just returns the enumerator. Each time the caller asks for the next item, the body runs until the next yield return, hands back that value and pauses there. That's great for large or endless sequences, because I never build the whole list in memory, and it's how LINQ operators themselves are written. The surprise is that argument validation is lazy too. If I check for a null argument at the top of an iterator method, the exception doesn't come when the method is called. It comes later, when someone first enumerates, which could be far away in the code and confusing to debug. The fix is to have a normal public method that validates and then returns a call to a private or local iterator function that does the yielding."

Code:
public static IEnumerable<string> ReadLines(string path)
{
    ArgumentNullException.ThrowIfNull(path); // runs on the call
    return Iterate();

    IEnumerable<string> Iterate()
    {
        using var reader = new StreamReader(path);
        string? line;
        while ((line = reader.ReadLine()) != null)
            yield return line; // pauses here until the next item is asked for
    }
}
Red flag to avoid:

Thinking the method body runs fully when called, or that yield return builds a list behind the scenes.

They may ask next:
  • What does yield break do?
  • If you enumerate an iterator method's result twice, does the body run twice?
Say it in 60 seconds

Async 4 questions

Medium Technical round Fresher, Mid-level, Senior Practice question

13. What actually happens when a method hits an await? Does async create a new thread?

What the interviewer is really testing:
Whether you know async is about not blocking threads while waiting, not about running code in parallel, and can describe the flow correctly.
Answer frame:

Before the await: the method runs synchronously on the caller's thread.

At an incomplete await: the method returns an unfinished Task and the thread goes back to do other work.

Resume: when the awaited work completes, the rest of the method runs as a continuation, on the captured context or a thread-pool thread.

Sample spoken answer:

"An async method starts running synchronously on the calling thread, just like a normal method. When it reaches an await on a task that isn't finished yet, say a database call or an HTTP request, it doesn't block. It registers the rest of the method as a continuation and returns an unfinished Task to its caller, so the thread is free to do other work. When the I/O completes, the continuation is scheduled. In a UI app it resumes on the UI thread, because the synchronization context was captured. In an ASP.NET Core app or a console app, it resumes on a thread-pool thread, often a different one. So async by itself doesn't create a thread. For I/O there's usually no thread waiting at all during the wait. The compiler makes this work by rewriting the method into a state machine. If I actually want CPU-heavy work off the current thread, that's Task.Run, which is a separate decision."

Red flag to avoid:

Saying async runs the method on a background thread, or that await blocks the thread until the result arrives.

They may ask next:
  • If an awaited task has already completed, does the method still give up the thread?
  • Why is async useful on a web server even though each request still has to wait for the database?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

14. Why can calling .Result or .Wait() on an async method freeze an application? How do you fix it?

What the interviewer is really testing:
Whether you understand sync-over-async and synchronization contexts well enough to explain the classic deadlock, not just repeat 'never use .Result'.
Answer frame:

Setup: a thread with a synchronization context, like a UI thread, blocks on a task with .Result.

Deadlock: the async method's continuation needs that same thread to resume, but the thread is blocked waiting for it.

Fixes: async all the way up; ConfigureAwait(false) in library code; avoid blocking even where no context exists, to prevent thread-pool starvation.

Sample spoken answer:

"The classic case is a desktop app. A button handler on the UI thread calls an async method and then blocks on .Result. Inside, the method awaits something, and by default it captures the UI synchronization context, so when the await finishes it wants to continue on the UI thread. But the UI thread is stuck in .Result waiting for the task to finish. Each is waiting on the other, so the app freezes. The real fix is to go async all the way: make the handler async and await the call. In library code I'd also use ConfigureAwait(false), so continuations don't need to come back to the caller's context. ASP.NET Core doesn't have a synchronization context, so this exact deadlock doesn't happen there, but blocking is still bad. Each blocked request holds a thread-pool thread, and under load the pool runs out, which shows up as the whole service slowing to a crawl."

Code:
// WinForms: this freezes the window forever
private void LoadButton_Click(object sender, EventArgs e)
{
    string text = LoadAsync().Result; // UI thread blocks here
    label1.Text = text;
}

private async Task<string> LoadAsync()
{
    await Task.Delay(500);   // wants to resume on the UI thread
    return "done";
}

// Fix: async all the way
private async void LoadButtonFixed_Click(object sender, EventArgs e)
{
    label1.Text = await LoadAsync();
}
Red flag to avoid:

Saying .Result is fine because the task will finish eventually, or wrapping everything in Task.Run without understanding the cause.

They may ask next:
  • What does ConfigureAwait(false) actually change?
  • What is thread-pool starvation and how would you spot it in a running service?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

15. Why is async void considered dangerous, and when is it acceptable?

What the interviewer is really testing:
Whether you know async void methods can't be awaited and their exceptions escape the caller, which can crash a process.
Answer frame:

No Task: the caller gets nothing to await, so it can't know when the method finished.

Exceptions: an exception can't be caught by the caller; it's raised on the synchronization context and can crash the process.

Allowed: event handlers, because their delegate signature returns void; everywhere else return Task.

Sample spoken answer:

"An async void method gives the caller nothing to hold on to. With async Task, the caller gets a Task it can await, so it knows when the work finished and any exception comes back through that Task. With async void, the caller fires it and moves on. It can't wait for it, and if the method throws, the exception can't be caught with a try-catch around the call. It gets raised on the synchronization context, or on the thread pool if there isn't one, and in many apps that crashes the process. It also makes testing harder, because a test can't await it. The one place it's acceptable is an event handler, like a button click, because the event's delegate type returns void and I can't change it. Even then I put a try-catch inside the handler so nothing escapes. Everywhere else, an async method returns Task or Task of T."

Red flag to avoid:

Saying async void is fine for fire-and-forget work, or not knowing that its exceptions bypass the caller.

They may ask next:
  • How would you unit test a method that is async void?
  • What happens if you pass an async lambda to a parameter of type Action?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

16. You need to call an API for 500 IDs. Doing them one by one is too slow, but the API allows only 10 calls at once. How would you write it?

What the interviewer is really testing:
Whether you can run I/O-bound work concurrently with a limit, handle results and failures, and avoid reaching for threads when async is the right tool.
Answer frame:

Concurrency: start the calls as tasks and await them together with Task.WhenAll.

Limit: a SemaphoreSlim with 10 slots; take a slot before each call, release it in finally.

Failures: WhenAll throws the first exception when awaited; decide whether one failure fails all or gets retried and logged.

Sample spoken answer:

"Since this is I/O, I don't want threads, I want lots of calls in flight with a cap. I'd use a SemaphoreSlim set to 10. For each ID I start an async lambda that waits for a slot with WaitAsync, makes the call, and releases the slot in a finally block, so a failed call never leaks a slot. Then I await Task.WhenAll on all the tasks, which gives me the results in the same order as the input. On errors: when you await WhenAll and something failed, you get the first exception thrown, and the task's Exception property holds all of them. If one failure shouldn't sink the batch, I'd catch inside the lambda and return a result object that says success or failure, and add a retry with backoff for transient errors. On newer .NET, Parallel.ForEachAsync with MaxDegreeOfParallelism set to 10 gives the same cap with less code, though I collect the results myself."

Code:
async Task<string[]> FetchAllAsync(HttpClient http, IEnumerable<int> ids,
                                   CancellationToken ct)
{
    using var gate = new SemaphoreSlim(10);

    var tasks = ids.Select(async id =>
    {
        await gate.WaitAsync(ct);
        try
        {
            return await http.GetStringAsync("items/" + id, ct);
        }
        finally
        {
            gate.Release();
        }
    });

    return await Task.WhenAll(tasks);
}
Red flag to avoid:

Starting all 500 calls at once with no limit, or using .Result inside a Parallel.ForEach and blocking threads.

They may ask next:
  • Why is Parallel.ForEach with a blocking HTTP call a poor choice here?
  • How would you add a timeout or cancellation to the whole batch?
  • What changes if the API also limits calls per second, not just at once?
Say it in 60 seconds

Resources & GC 4 questions

Easy Technical round Fresher, Mid-level Practice question

17. What is IDisposable for, and what does a using statement do for you?

What the interviewer is really testing:
Whether you know the garbage collector frees memory but not files, sockets or connections on your schedule, and that using gives deterministic cleanup.
Answer frame:

Purpose: release resources like file handles, connections and sockets as soon as you're done, not whenever the GC runs.

using: calls Dispose in a finally block, so cleanup happens even if an exception is thrown.

Forms: the block form, the using declaration that disposes at the end of the scope, and await using for IAsyncDisposable.

Sample spoken answer:

"The garbage collector frees memory, but it runs when it decides to, and it doesn't call Dispose for me. Some objects hold things that are scarce, like a file handle, a database connection or a socket, and I want those released as soon as I'm done. IDisposable gives those types a Dispose method that does the cleanup. A using statement guarantees Dispose is called. The compiler turns it into a try-finally, so even if an exception is thrown inside the block, Dispose still runs. Since C# 8 I can also write a using declaration, just using var reader equals something, and it gets disposed at the end of the enclosing scope, which saves a level of nesting. For types that need to clean up asynchronously, like some streams, there's IAsyncDisposable and await using. The rule I follow is simple: if I create an object that implements IDisposable and I own it, I dispose it."

Code:
using (var reader = new StreamReader("data.csv"))
{
    Console.WriteLine(reader.ReadLine());
} // Dispose runs here, even after an exception

// C# 8 using declaration: disposed at the end of the method
using var writer = new StreamWriter("out.txt");
writer.WriteLine("hello");
Red flag to avoid:

Saying the garbage collector calls Dispose automatically, or that using is only a scoping convenience.

They may ask next:
  • What happens to a database connection if you forget to dispose it?
  • Should you dispose an object you received as a parameter, or one you got from a factory?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

18. How does the .NET garbage collector use generations, and what is special about the large object heap?

What the interviewer is really testing:
Whether you understand why short-lived allocations are cheap, why long-lived ones and large ones cost more, and how that shapes code in hot paths.
Answer frame:

Reachability: the GC marks everything reachable from roots such as locals, statics and GC handles; the rest is garbage.

Generations: new objects start in gen 0, survivors move to gen 1 then gen 2; young collections are frequent and cheap, gen 2 ones are expensive.

Large object heap: big objects go straight to a separate heap collected with gen 2 and not compacted by default, so churn there hurts.

Sample spoken answer:

"The .NET GC is a tracing collector. It starts from roots, like local variables in active methods, static fields and handles, marks everything reachable, and reclaims the rest, cycles included. It splits the managed heap into generations because most objects die young. New objects go into gen 0, which is collected often and quickly because it's mostly garbage. Anything that survives moves to gen 1, and then to gen 2, which holds long-lived objects and is collected much less often but at a higher cost. The large object heap is for big allocations, roughly 85 thousand bytes and up, typically large arrays. Those are logically part of gen 2, only cleaned up in a full collection, and by default they aren't compacted, so allocating and dropping lots of large buffers causes fragmentation and expensive gen 2 collections. In hot paths I'd reuse big buffers, for example with ArrayPool, rather than allocating fresh ones each time."

Red flag to avoid:

Saying .NET uses reference counting, or that objects are freed the moment they go out of scope.

They may ask next:
  • What is the difference between workstation and server garbage collection?
  • Why is calling GC.Collect yourself usually a bad idea?
Say it in 60 seconds
Hard Technical round Senior Practice question

19. Explain the full Dispose pattern with a finalizer. Why does it call GC.SuppressFinalize, and when do you really need a finalizer?

What the interviewer is really testing:
Whether you know how finalization works and its cost, and whether you know modern code rarely needs a finalizer thanks to SafeHandle.
Answer frame:

Finalizer: a safety net that runs on the finalizer thread at some unknown time if Dispose was never called; it delays collection.

Dispose(bool): true from Dispose frees managed and unmanaged resources; false from the finalizer frees only unmanaged ones.

SuppressFinalize: once disposed, the finalizer isn't needed; today, wrap raw handles in a SafeHandle instead of writing one.

Sample spoken answer:

"A finalizer is a safety net for unmanaged resources, like a raw native handle, in case someone forgets to call Dispose. It has a real cost. An object with a finalizer can't be freed on its first collection. It goes onto a queue, the finalizer thread runs it later, and only then can the memory be reclaimed, so it survives longer and gets promoted. The pattern has a public Dispose that calls a protected Dispose with true and then GC.SuppressFinalize, so the GC knows the finalizer is no longer needed. The finalizer calls Dispose with false. The flag matters because during finalization, other managed objects I reference may already have been finalized, so on that path I only release the unmanaged resource. I'd add that in modern code I almost never write a finalizer. I wrap the native handle in a SafeHandle subclass, which already has reliable finalization, and my class just implements plain IDisposable."

Code:
public class NativeBuffer : IDisposable
{
    private IntPtr _ptr = Marshal.AllocHGlobal(1024);
    private bool _disposed;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // no need to finalize any more
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        if (disposing)
        {
            // dispose other managed IDisposable fields here
        }
        Marshal.FreeHGlobal(_ptr); // unmanaged: freed on both paths
        _disposed = true;
    }

    ~NativeBuffer() => Dispose(false);
}
Red flag to avoid:

Adding a finalizer to every disposable class, or saying the finalizer runs as soon as the object goes out of scope.

They may ask next:
  • Why is it unsafe to touch other managed objects from inside a finalizer?
  • What does a SafeHandle give you that a raw IntPtr field doesn't?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

20. A service creates a new HttpClient inside a using block for every outgoing call. Under load, calls start failing with socket errors. What's going on, and what do you change?

What the interviewer is really testing:
Whether you know that disposing isn't always the right lifetime, and can diagnose a resource problem that only appears under load.
Answer frame:

Cause: each disposed client closes its connections, which linger in a waiting state, so busy traffic runs out of sockets.

Fix: reuse long-lived clients so connections are pooled, with a pooled-connection lifetime so DNS changes are picked up.

Verify: reproduce with a load test, watch connection counts, then confirm the errors stop.

Sample spoken answer:

"This is a known trap. The code looks right, because HttpClient is IDisposable and we're disposing it. But each client has its own connection pool, and disposing it closes those connections. Closed TCP connections stay in a waiting state for a while at the operating system level, so under heavy traffic the machine runs out of available sockets and new calls fail. HttpClient is designed to be created once and reused, and it's safe to use from many threads at the same time. So I'd change it to a long-lived client, either a shared instance or one handed out by the framework's client factory, if the app already uses it. With a single long-lived instance there's one catch: it can hold on to old DNS results, so I'd set a pooled-connection lifetime on the handler so connections are refreshed now and then. Then I'd run a load test and compare socket counts before and after."

Red flag to avoid:

Saying every IDisposable must be disposed right after each use no matter what, or blaming the remote API without checking our side.

They may ask next:
  • Is it safe to share one HttpClient between many threads? What about changing its headers?
  • How would you check how many connections a process has open on the machine?
Say it in 60 seconds

Generics & Nullability 4 questions

Medium Coding round Fresher, Mid-level Practice question

21. Write a generic method that returns the largest item in a sequence. What constraint do you need, and what other constraints does C# offer?

What the interviewer is really testing:
Whether you can write a real generic method, know why a constraint is needed to call members on T, and know how C# generics differ at runtime.
Answer frame:

Why a constraint: without one, T is only known to be an object, so you can't compare two T values.

The constraint: where T : IComparable of T lets you call CompareTo without boxing value types.

Others: class, struct, new(), a base class, notnull, unmanaged; generics keep their type at runtime.

Sample spoken answer:

"Inside a generic method, the compiler only knows T is some object, so I can't compare two T values with greater-than. I add the constraint where T implements IComparable of T, and then I can call CompareTo. I take an enumerator, throw if the sequence is empty, then walk it and keep the best so far. Using the generic IComparable of T rather than the old non-generic one matters for value types: an int compares directly, without boxing. Other constraints I use are class and struct to require a reference or value type, new() when I need to create a T with a parameterless constructor, a base class or interface, and notnull or unmanaged for more specialised cases. One thing that's different from Java: C# generics aren't erased. The type argument exists at runtime, so typeof(T) works, and the runtime generates specialised code for each value type, which is why a List of int doesn't box."

Code:
public static T Largest<T>(IEnumerable<T> items) where T : IComparable<T>
{
    using var e = items.GetEnumerator();
    if (!e.MoveNext())
        throw new InvalidOperationException("Sequence is empty");

    T best = e.Current;
    while (e.MoveNext())
    {
        if (e.Current.CompareTo(best) > 0)
            best = e.Current;
    }
    return best;
}

int top = Largest(new[] { 3, 9, 4 }); // 9
Red flag to avoid:

Comparing T values with greater-than and not seeing why it won't compile, or saying C# erases generic types like Java.

They may ask next:
  • Why not just use a parameter of type object, or dynamic, instead of a generic?
  • How would you let the caller pass a key selector so they can find the largest by a property?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

22. Why can you assign an IEnumerable of string to an IEnumerable of object, but not a List of string to a List of object?

What the interviewer is really testing:
Whether you understand generic variance, the out and in keywords, and why read-write types must stay invariant for type safety.
Answer frame:

Covariance (out): a type that only hands T out, like IEnumerable, can be treated as a more general type.

Contravariance (in): a type that only takes T in, like Action or IComparer, can accept a more specific type.

Invariance: List both reads and writes T, so allowing the conversion would let you add the wrong type; arrays show the danger.

Sample spoken answer:

"It comes down to generic variance. IEnumerable is declared with out T, which means it only ever gives T values out. If I can read strings from something, it's safe to treat it as a source of objects, so the conversion is allowed. That's covariance. Contravariance is the reverse, with the in keyword. An Action of object can be used where an Action of string is expected, because anything that can handle any object can handle a string. List of T is invariant because it both reads and writes. If I could treat a List of string as a List of object, I could then add an integer to it, and the list would be broken. Arrays actually allow that conversion for historical reasons, and it's only caught at runtime with an ArrayTypeMismatchException. Two more rules: variance only works on interfaces and delegates, and only for reference types, so IEnumerable of int doesn't convert to IEnumerable of object."

Code:
IEnumerable<string> names = new List<string> { "a" };
IEnumerable<object> objs = names;         // OK: covariant (out T)

Action<object> printAny = o => Console.WriteLine(o);
Action<string> printText = printAny;      // OK: contravariant (in T)

// List<object> bad = new List<string>(); // compile error: invariant

object[] arr = new string[1];             // allowed for arrays...
arr[0] = 42;                              // ...ArrayTypeMismatchException
Red flag to avoid:

Saying the List conversion is blocked by an arbitrary compiler rule, with no example of what would go wrong.

They may ask next:
  • Why doesn't an IEnumerable of int convert to an IEnumerable of object?
  • Where would you use the in keyword on your own interface?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level, Senior Practice question

23. What do nullable reference types change in C#, and what don't they protect you from?

What the interviewer is really testing:
Whether you know the feature is compile-time analysis only, and can use it well without trusting it blindly at runtime.
Answer frame:

What it is: with the feature enabled, string means 'should not be null' and string? means 'may be null'; the compiler warns on misuse.

Flow analysis: after a null check the compiler knows a value is safe; the ! operator silences a warning.

Limits: warnings only, no runtime change; data from outside, reflection or old libraries can still be null.

Sample spoken answer:

"Before C# 8, every reference type could be null and the compiler never said a word. With nullable reference types enabled, usually with the Nullable setting in the project file, a plain string means I intend it to never be null, and string with a question mark means null is allowed. The compiler then tracks the flow. If I dereference a string question mark without checking, I get a warning. If I assign null to a plain string, I get a warning. After an if-not-null check, it knows the value is safe. The exclamation mark, the null-forgiving operator, tells the compiler to trust me, and I'd use it sparingly. What it doesn't do is change anything at runtime. It's annotations and warnings only. JSON deserialisation, reflection or a library without annotations can still hand me a null, so public methods still need guard clauses like ArgumentNullException.ThrowIfNull. It's also separate from Nullable of T for value types, which is a real runtime struct."

Code:
#nullable enable
public class Greeter
{
    public string Greet(string? name)
    {
        // return "Hi " + name.Length; // warning: name may be null
        if (name is null) return "Hi there";
        return "Hi " + name;           // compiler knows it is not null here
    }

    public void Save(string path)
    {
        ArgumentNullException.ThrowIfNull(path); // runtime guard still needed
    }
}
Red flag to avoid:

Saying nullable reference types stop null reference exceptions at runtime, or mixing them up with Nullable of T.

They may ask next:
  • How is int? different from string? under the hood?
  • When is using the null-forgiving operator justified?
Say it in 60 seconds
Hard Situational round Senior Practice question

24. Your team wants to turn on nullable reference types in a large, older codebase, and it produces thousands of warnings. How would you roll it out?

What the interviewer is really testing:
Whether you can plan a gradual, low-risk adoption of a language feature in real code, rather than a big-bang change or giving up.
Answer frame:

Scope: enable it per project or per file with the nullable directive, starting with new code and leaf libraries.

Order: annotate shared models and public APIs first, because fixes there remove warnings everywhere downstream.

Guard rails: no new warnings in touched files, treat nullable warnings as errors once a project is clean, avoid blanket use of !.

Sample spoken answer:

"I wouldn't flip it on for the whole solution and ask people to fix thousands of warnings in one go. The PR would be huge, and people would silence warnings with the exclamation mark just to get it merged, which defeats the point. I'd do it gradually. First, turn it on for all new files and for small leaf projects that nothing else depends on, so everyone gets used to it. Then work on the shared layers, like the domain models and public service interfaces, because once those say clearly what can be null, a lot of warnings in the code that uses them either vanish or become real findings. For the rest, the rule would be: when you touch a file, enable nullable in it and fix its warnings. Once a project is clean, set nullable warnings as errors there so it stays clean. I'd track the warning count on each build so progress is visible, and I'd expect it to surface some real null bugs along the way."

Red flag to avoid:

Turning it on everywhere and silencing warnings with the null-forgiving operator, or deciding it isn't worth it without trying.

They may ask next:
  • How do you handle a property that's null only until an initialise method runs?
  • Would you ever turn on nullable for everything at once? What would make that reasonable?
Say it in 60 seconds

Exceptions & Collections 3 questions

Easy Technical round Fresher, Mid-level Practice question

25. In a catch block, what's the difference between throw; and throw ex;? How else do you rethrow well?

What the interviewer is really testing:
Whether you preserve stack traces when rethrowing, which decides whether a production error can be traced to its source.
Answer frame:

Bare throw: rethrows the same exception and keeps the original stack trace.

throw ex: rethrows it but resets the stack trace to the catch block, hiding where it started.

Better options: wrap in a new exception with the original as InnerException; use exception filters with when to catch only what you handle.

Sample spoken answer:

"If I catch an exception, log it, and want it to keep going up, I write throw with nothing after it. That rethrows the same exception and keeps the original stack trace, so whoever reads the logs sees the exact line where it started. If I write throw ex, the stack trace is reset to my catch block, and the original location is gone, which makes production bugs much harder to trace. When I want to add context, like which order ID failed, I throw a new, more meaningful exception and pass the caught one as the inner exception, so nothing is lost. I also like exception filters, catch with a when clause. The filter runs before the stack is unwound, so if the condition is false, the exception passes through untouched, which beats catching and rethrowing. And I only catch what I can actually handle."

Code:
try
{
    ProcessOrder(orderId);
}
catch (HttpRequestException ex) when (IsTransient(ex))
{
    logger.LogWarning(ex, "Retrying order {OrderId}", orderId);
    throw;                      // keeps the original stack trace
}
catch (FormatException ex)
{
    throw new OrderImportException("Bad data in order " + orderId, ex);
}
Red flag to avoid:

Saying throw ex; and throw; are the same, or wrapping exceptions without keeping the original as the inner exception.

They may ask next:
  • What does an exception filter with when give you that an if statement inside the catch doesn't?
  • When is it fine to catch the base Exception type?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

26. You need to check, for each of 100,000 incoming IDs, whether you've already seen it. Which collection do you use, and why not a List?

What the interviewer is really testing:
Whether you pick collections by the operation you do most, and know the equality rules that hash-based collections depend on.
Answer frame:

List: Contains scans every item, so checking n IDs is O(n squared) overall.

HashSet: Add and Contains are O(1) on average; Add returns false if the item was already there.

Dictionary: when you need a value per key, like a count; keys need stable Equals and GetHashCode.

Sample spoken answer:

"I'd use a HashSet. With a List, Contains walks through the items one by one, so each check is linear, and across 100,000 IDs the whole thing becomes quadratic. A HashSet hashes the value and goes almost straight to it, so Add and Contains are constant time on average. It also has a handy detail: Add returns false if the item was already in the set, so I can check and insert in one call. If I needed to store something per ID, like how many times I'd seen it or the last timestamp, I'd use a Dictionary instead, with TryGetValue so I only look up once. Both depend on the key's Equals and GetHashCode. For ints or strings that's already right, but for my own class as a key, I'd override both together or use a record, and never change a key after putting it in the set."

Code:
var seen = new HashSet<long>();
foreach (long id in incomingIds)
{
    if (!seen.Add(id))          // false means it was already there
        Console.WriteLine("Duplicate: " + id);
}
Red flag to avoid:

Choosing a List with Contains for large lookups, or not knowing hash collections rely on Equals and GetHashCode.

They may ask next:
  • What happens if you change a property that GetHashCode uses after the object is already in a HashSet?
  • When is a List still the better choice?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

27. In a code review, you see a teammate's service catches Exception around every call, logs it and carries on. The tests pass. What do you say?

What the interviewer is really testing:
Whether you can explain why swallowing exceptions hides failures and corrupts state, and give that feedback in a way that gets the code changed.
Answer frame:

The risk: the program keeps going in a broken state; callers think it worked; bugs surface far from the cause.

What good looks like: catch specific exceptions you can handle, let the rest bubble up to one top-level handler.

How to say it: a concrete example from their code, a suggested change, and a conversation rather than a blocking comment.

Sample spoken answer:

"I'd raise it, but with a concrete example from their code rather than a general rule. Catching Exception everywhere and carrying on means that if saving an order fails, the method still returns as if it worked. The caller moves on, the data is now inconsistent, and the only trace is a log line nobody reads. It also catches things we can't recover from, like a null reference that's really a bug. I'd suggest catching only the specific exceptions we can handle at that point, like a timeout we'll retry, and letting everything else bubble up to one top-level handler that logs it properly and returns a clean error. I'd point out that the tests pass because they only cover the happy path, and offer to add one test where a dependency throws. I'd leave it as a comment and a quick chat, not a lecture, because they probably added the catches after a crash and meant well."

Red flag to avoid:

Approving it because the tests pass, or rejecting it with 'bad practice' and no explanation of what could go wrong.

They may ask next:
  • Where exactly would you put the top-level handler in a service or a console app?
  • What if the teammate says the service must never crash in production?
Say it in 60 seconds

Real Work 3 questions

Hard Behavioral round Mid-level, Senior Practice question

28. Tell me about a memory leak or steadily growing memory you tracked down in a .NET application. How did you find the cause?

What the interviewer is really testing:
Whether you have debugged real memory problems in a managed runtime with evidence, and understand that leaks in .NET are references that are kept alive.
Answer frame:

Symptom: what you saw, like memory climbing until a restart, and why it wasn't obvious.

Evidence: counters, memory dumps and comparing heap snapshots to see which objects kept growing and what held them.

Fix and guard: the root cause, the fix and what you changed so it doesn't come back.

Sample spoken answer:

"At my last company a background service's memory climbed slowly for days until it got restarted, and gen 2 collections kept getting longer. Since .NET has a garbage collector, I knew it had to be something still holding references. I took two memory dumps a few hours apart and compared them. One type of handler object had grown from a few hundred to tens of thousands. Following the path back to a root showed every instance was referenced by a static event on a settings class. Each job created a handler that subscribed to a settings-changed event but never unsubscribed, so the static event kept every one of them alive. The fix was to make the handler IDisposable, unsubscribe in Dispose, and wrap each job in a using. Memory went flat after that. I also added a memory graph to our dashboard and an alert on steady growth, so we'd catch the next one early."

Red flag to avoid:

A story that ends with raising memory limits or scheduling restarts, without ever finding what held the references.

They may ask next:
  • What tools would you use to take and compare memory dumps of a .NET process?
  • How would you tell a real leak from memory that's just being held by a cache on purpose?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

29. Have you converted synchronous C# code to async? How did you do it without breaking things halfway?

What the interviewer is really testing:
Whether you understand that async spreads through a call chain, and can plan a gradual change that avoids blocking on async code in the middle.
Answer frame:

Why: the problem that made it worth doing, such as threads blocked on I/O under load.

How: start from the lowest I/O calls and work upward, keeping signatures async all the way to the entry point.

Result: how you measured it and what you watched for, like any leftover .Result calls.

Sample spoken answer:

"At my last company one of our services started timing out under load even though the CPU was nearly idle. The requests were spending their time waiting on database and HTTP calls, all synchronous, so every waiting request held a thread and the pool couldn't keep up. I proposed moving the data access to async. I started at the bottom, the repository methods, switching to the async versions of the database and HTTP calls, and then worked upward one layer at a time, changing each caller to await and return Task, until I reached the controllers. The rule I held the team to was no .Result or .Wait in between, because blocking halfway gives you the worst of both. I passed a CancellationToken through while I was touching every signature anyway. After release, the same traffic ran with far fewer threads, and the timeouts stopped. We also added an analyser rule that flags blocking calls on tasks."

Red flag to avoid:

Wrapping synchronous calls in Task.Run and calling it async, or leaving .Result calls in the middle of the chain.

They may ask next:
  • What would you do if an interface you must implement only has a synchronous method?
  • How did you pass cancellation through the layers, and why does it matter?
Say it in 60 seconds
Medium Behavioral round Fresher, Mid-level Practice question

30. Tell me about a time you made C# code measurably faster. How did you find where the time or the allocations were going?

What the interviewer is really testing:
Whether you measure before changing code and know the common C#-specific causes of slowness, like repeated enumeration and needless allocations.
Answer frame:

Measure first: a profiler, a benchmark or allocation counts, not guessing.

Cause: the specific hot spot and why it cost so much in C# terms.

Proof: before and after numbers from the same benchmark, and a check that output didn't change.

Sample spoken answer:

"In my final-year project I had an import step that matched records from two files, and it took minutes on a real file. I ran it under a profiler first instead of guessing. Most of the time was in a loop where, for each record, I called FirstOrDefault on a list of the other file's records. That's a linear scan inside a loop, so it was quadratic. The same profile showed lots of short-lived strings from building a lookup key with concatenation on every pass. I built a Dictionary keyed by ID once before the loop, so each lookup became a single hash lookup, and computed each key once. I wrote a small benchmark with BenchmarkDotNet to compare the old and new versions on the same data. The run time dropped from minutes to a couple of seconds, allocations fell a lot, and I diffed the output files to confirm the results were identical."

Red flag to avoid:

Changing code for speed with no measurement, or claiming a speed-up with no before and after on the same data.

They may ask next:
  • Why is a benchmark library better than timing code with a Stopwatch in a loop?
  • When is an optimisation not worth the extra complexity?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card