C# and .NET interview questions: real-world scenarios
These are questions interviewers ask to see whether you understand how .NET behaves in production, not just syntax. Each answer explains the why and shows a short example.
Intermediate · 12 questions · Tap a question to see the answer.
Q1. A legacy ASP.NET (.NET Framework) app hangs when a controller calls
GetDataAsync().Result. Why, and how do you fix it?Classic ASP.NET and UI apps have a SynchronizationContext that lets only one thread run the request at a time.
.Resultblocks that thread while the awaited task wants to resume on the same context, so each waits for the other: a deadlock.Fix it by going async all the way: make the action
async Task<IActionResult>andawaitthe call. In library code that never needs the context,ConfigureAwait(false)also avoids the deadlock. ASP.NET Core has no SynchronizationContext, so this exact hang does not occur there, but blocking on tasks still wastes thread-pool threads and should be avoided.public async Task<IActionResult> Index() { var data = await _service.GetDataAsync(); return View(data); }Q2. What is the difference between
IEnumerable<T>andIQueryable<T>when querying a database with EF Core?IQueryable<T>builds an expression tree that EF Core translates to SQL, soWhere,OrderByandTakerun in the database and only matching rows come back.Once you switch to
IEnumerable<T>(for example withAsEnumerable()orToList()), later operators run in memory on rows already loaded. Filtering afterToList()on a large table loads the whole table first. Keep filters on theIQueryableand materialize at the end.// Good: WHERE and TOP run in SQL var recent = await db.Orders .Where(o => o.CreatedAt > since) .OrderByDescending(o => o.CreatedAt) .Take(50) .ToListAsync();Q3. When would you use a
recordinstead of aclassin C#?Use a
recordfor data whose identity is its values: DTOs, messages, events, value objects. Records give value-based equality, a readableToString(), deconstruction and non-destructive mutation withwith.Use a
classfor entities with identity and behaviour that change over time, such as an EF Core entity tracked by id.record structis available when you want value semantics without a heap allocation.public record Money(decimal Amount, string Currency); var a = new Money(10, "USD"); var b = a with { Amount = 20 }; Console.WriteLine(a == new Money(10, "USD")); // TrueQ4. Explain Singleton, Scoped and Transient lifetimes, and the bug caused by injecting a Scoped service into a Singleton.
Singleton: one instance for the app's lifetime. Scoped: one instance per scope, which in ASP.NET Core means per HTTP request. Transient: a new instance every time it is resolved.
If a Singleton takes a Scoped service (such as a
DbContext) in its constructor, that scoped instance is captured forever and shared across requests and threads. This is a 'captive dependency'. With aDbContextit causes threading errors and stale data. ASP.NET Core's scope validation throws at startup in Development to catch it. Fix it by making the consumer Scoped or by creating a scope withIServiceScopeFactorywhen needed.public class Worker(IServiceScopeFactory scopes) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken ct) { using var scope = scopes.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); // use db for this unit of work } }Q5. Why is building a large string with
+=in a loop slow, and what should you use?Strings are immutable, so every
+=allocates a new string and copies everything so far. For n appends this is roughly O(n²) work and creates lots of garbage.Use
StringBuilderfor loops,string.Joinfor joining a collection, or string interpolation for a fixed number of parts.var sb = new StringBuilder(); foreach (var line in lines) sb.AppendLine(line); var text = sb.ToString();Q6. What does
IDisposabledo, and why shouldHttpClientusually not be wrapped inusingper request?IDisposablereleases unmanaged resources (sockets, file handles) deterministically;usingcallsDisposeat the end of the scope.HttpClientis the exception that proves the rule: creating and disposing one per request can exhaust sockets because connections linger in TIME_WAIT. Reuse clients throughIHttpClientFactory(typed or named clients) or a long-lived instance withSocketsHttpHandler.PooledConnectionLifetimeset so DNS changes are picked up.builder.Services.AddHttpClient<GitHubClient>(c => c.BaseAddress = new Uri("https://api.github.com/"));Q7. You need data from three independent APIs. How do you call them in parallel, and how are exceptions reported?
Start all three tasks first, then
await Task.WhenAll(...). Total time is roughly the slowest call instead of the sum.If several tasks fail,
awaitrethrows only the first exception; the full set is in the returned task'sException.InnerExceptions. Pass aCancellationTokenso the others can stop when one fails if that is what you need.var userTask = api.GetUserAsync(id, ct); var ordersTask = api.GetOrdersAsync(id, ct); var prefsTask = api.GetPrefsAsync(id, ct); await Task.WhenAll(userTask, ordersTask, prefsTask); var user = userTask.Result; // already completed, safeQ8. What do nullable reference types (
string?) change at runtime?Nothing at runtime. They are a compile-time feature: with
<Nullable>enable</Nullable>the compiler tracks whether a reference may be null and warns on likely null dereferences.stringmeans 'should not be null' andstring?means 'may be null'. The!operator silences a warning but adds no check, so you still validate external input, for example withArgumentNullException.ThrowIfNull.Q9. What is the difference between value types and reference types, and when would you make a
struct?Value types (
int,DateTime,struct) hold their data directly and are copied on assignment. Reference types (class,string, arrays) are stored on the heap and variables hold a reference, so two variables can point at the same object.Make a
structfor small, immutable values that are created in large numbers, such as a point or a money amount, where avoiding heap allocations helps. Preferreadonly struct. Large or mutable structs are a trap because every copy duplicates the data and changes to a copy are lost.Q10. How do you let a long-running operation stop when the HTTP client disconnects in ASP.NET Core?
Accept a
CancellationTokenparameter in the action. ASP.NET Core binds it toHttpContext.RequestAborted, which is cancelled when the client disconnects.Pass that token down to every async call (EF Core queries,
HttpClient, delays) so work stops early and resources are freed.[HttpGet("report")] public async Task<IActionResult> Report(CancellationToken ct) { var rows = await db.Sales.Where(s => s.Year == 2026).ToListAsync(ct); return Ok(rows); }Q11. What is
Span<T>and what problem does it solve?Span<T>is a type-safe view over a contiguous block of memory (an array, a stack buffer or native memory) without copying it. Slicing a span is free, so parsing code can work on parts of a buffer without allocating substrings or sub-arrays.It is a
ref struct, so it can only live on the stack: no fields in classes, no capture in lambdas, and no use acrossawait. UseMemory<T>when you need to store it or use it in async code.ReadOnlySpan<char> line = "2026-10-16,42"; int comma = line.IndexOf(','); int value = int.Parse(line[(comma + 1)..]); // no substring allocatedQ12. What is the difference between .NET LTS and STS releases, and which should a production team pick?
Even-numbered releases (.NET 8, .NET 10) are Long Term Support with 3 years of patches. Odd-numbered releases (.NET 9, .NET 11) are Standard Term Support. Microsoft extended STS support to 24 months starting with .NET 9.
Most production teams standardise on LTS and upgrade every two years. STS suits teams that want new features early and can upgrade on schedule. Either way, install the monthly servicing updates, because they contain security fixes.
Original questions written to explain concepts; not taken from any certification exam. Spotted a mistake? See our corrections policy.