C# and .NET

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.

  1. 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. .Result blocks 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> and await the 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);
    }

    Official documentation

  2. Q2. What is the difference between IEnumerable<T> and IQueryable<T> when querying a database with EF Core?

    IQueryable<T> builds an expression tree that EF Core translates to SQL, so Where, OrderBy and Take run in the database and only matching rows come back.

    Once you switch to IEnumerable<T> (for example with AsEnumerable() or ToList()), later operators run in memory on rows already loaded. Filtering after ToList() on a large table loads the whole table first. Keep filters on the IQueryable and 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();

    Official documentation

  3. Q3. When would you use a record instead of a class in C#?

    Use a record for data whose identity is its values: DTOs, messages, events, value objects. Records give value-based equality, a readable ToString(), deconstruction and non-destructive mutation with with.

    Use a class for entities with identity and behaviour that change over time, such as an EF Core entity tracked by id. record struct is 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")); // True

    Official documentation

  4. Q4. 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 a DbContext it 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 with IServiceScopeFactory when 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
        }
    }

    Official documentation

  5. 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 StringBuilder for loops, string.Join for 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();

    Official documentation

  6. Q6. What does IDisposable do, and why should HttpClient usually not be wrapped in using per request?

    IDisposable releases unmanaged resources (sockets, file handles) deterministically; using calls Dispose at the end of the scope.

    HttpClient is the exception that proves the rule: creating and disposing one per request can exhaust sockets because connections linger in TIME_WAIT. Reuse clients through IHttpClientFactory (typed or named clients) or a long-lived instance with SocketsHttpHandler.PooledConnectionLifetime set so DNS changes are picked up.

    builder.Services.AddHttpClient<GitHubClient>(c =>
        c.BaseAddress = new Uri("https://api.github.com/"));

    Official documentation

  7. 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, await rethrows only the first exception; the full set is in the returned task's Exception.InnerExceptions. Pass a CancellationToken so 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, safe

    Official documentation

  8. Q8. 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.

    string means 'should not be null' and string? means 'may be null'. The ! operator silences a warning but adds no check, so you still validate external input, for example with ArgumentNullException.ThrowIfNull.

    Official documentation

  9. 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 struct for small, immutable values that are created in large numbers, such as a point or a money amount, where avoiding heap allocations helps. Prefer readonly struct. Large or mutable structs are a trap because every copy duplicates the data and changes to a copy are lost.

    Official documentation

  10. Q10. How do you let a long-running operation stop when the HTTP client disconnects in ASP.NET Core?

    Accept a CancellationToken parameter in the action. ASP.NET Core binds it to HttpContext.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);
    }

    Official documentation

  11. 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 across await. Use Memory<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 allocated

    Official documentation

  12. Q12. 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.

    Official documentation

Original questions written to explain concepts; not taken from any certification exam. Spotted a mistake? See our corrections policy.