Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

.NET Backend Developer Top 100 Interview Questions

100 .NET backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable C# code. Covers the language and runtime, async and concurrency, the web pipeline, data access, caching, security, testing, diagnostics, performance and system design, from the basics to production incidents.

100 questions

Questions 21-40 of 100

  1. Q21.How do package dependencies and versioning work in .NET projects?

    Projects declare package references with versions in their project files, and the build restores them from configured package feeds into a local cache. Transitive dependencies are resolved automatically, picking the lowest version that satisfies every requirement.

    Keeping dependencies under control:

    1. Central package management: a single file at the repository root sets the version of each package, so every project uses the same version.
    2. Lock files: enabling them records the exact resolved versions, and builds in locked mode fail if the resolution changes unexpectedly.
    3. Floating versions with wildcards make builds non-reproducible; avoid them for production code.
    4. Vulnerability checks: the dotnet list package --vulnerable command, or equivalent auditing during restore, reports packages with known security issues.
    5. Feed configuration: use trusted feeds, and map package name prefixes to specific feeds to prevent dependency confusion attacks.

    Upgrading safely:

    • Upgrade regularly in small steps, rather than in large, risky jumps.
    • Read release notes for major versions, and run the full test suite.
    • Remove unused packages, since every dependency is code you must keep patched.
  2. Q22.How do configuration and options binding work in a .NET service?

    Configuration is read from several sources, such as settings files, environment variables, command-line arguments and secret stores, and combined into one key-value view, with later sources overriding earlier ones. Sections of it are then bound to strongly typed options classes that services receive through dependency injection.

    Typical source order, from lowest to highest priority:

    1. A base settings file.
    2. An environment-specific settings file, such as one for production.
    3. Secrets for local development.
    4. Environment variables, which containers and orchestrators set.
    5. Command-line arguments.

    Good practices:

    • Typed options: bind a section to a class such as PaymentOptions, instead of reading string keys throughout the code.
    • Validation at startup: use data annotations or custom validation, with validation on start, so a missing or invalid value fails the deployment instead of the first request.
    • Reloading: some options interfaces support changes at run time; use them only where reloading is safe.
    • Secrets: never in settings files committed to the repository; inject them from a secret store or environment.
    • Naming: environment variables use double underscores for sections, such as Payment__TimeoutSeconds.
  3. Q23.What are the dependency injection lifetimes in .NET, and what goes wrong with them?

    The built-in container creates services with one of three lifetimes, which decide how long an instance lives and who shares it. Choosing the wrong one causes subtle bugs, especially with state and thread safety.

    SingletonScopedTransient
    One instance for the whole applicationOne instance per scope, usually per HTTP requestA new instance every time it is requested
    Must be thread-safe, since all requests share itCan hold per-request state, such as a unit of workShould be lightweight and stateless
    Example: configuration, clients, cachesExample: database context, current userExample: small helpers and formatters

    Common mistakes:

    1. Captive dependencies: a singleton that depends on a scoped service keeps that scoped instance alive forever, sharing it across requests and threads; scope validation in development catches this.
    2. Resolving scoped services outside a scope, such as in a background worker; create a scope explicitly for each unit of work.
    3. Disposable transients resolved from the root container, which are kept until the application ends.
    4. Service locator pattern: injecting the container and resolving services manually hides dependencies.

    Rule of thumb: a service should never depend on another service with a shorter lifetime.

    Interview tip: explaining a captive dependency with a concrete example, such as a singleton cache holding a per-request database context, is one of the most common .NET interview checks.

  4. Q24.What does ahead-of-time compilation change for a .NET service?

    Native ahead-of-time compilation turns the application into a self-contained native executable at build time, instead of compiling intermediate language at run time. Startup becomes much faster and memory use drops, but some dynamic features are no longer available.

    Just-in-time compilationNative ahead-of-time compilation
    Compiles methods when they first run, and recompiles hot paths with better optimizationsCompiles everything at build time into one native executable
    Slower startup, very good peak performanceVery fast startup and lower memory use
    Supports reflection, dynamic loading and runtime code generationRestricts unbounded reflection and dynamic code generation
    Best for long-running services with heavy, steady trafficBest for scale-to-zero functions, command-line tools and dense container fleets

    What has to change for ahead-of-time builds:

    1. Trimming: unused code is removed, so code that relies on reflection must be annotated or replaced.
    2. Source generators instead of reflection for JSON serialization, configuration binding and similar tasks.
    3. Library compatibility: some libraries are not compatible; build warnings point them out.

    Middle ground: ready-to-run images precompile code but keep the just-in-time compiler, improving startup without the restrictions.

  5. Q25.How do source generators help, for example with JSON serialization?

    Source generators run inside the compiler and add C# code to your project based on what they find in it. Work that would otherwise happen through reflection at run time, such as discovering properties to serialize, happens at build time instead, which is faster, uses less memory and is compatible with trimming and ahead-of-time compilation.

    JSON with a generated context:

    1. Declare a partial class deriving from JsonSerializerContext.
    2. Mark it with [JsonSerializable(typeof(Order))] for each type you serialize.
    3. Pass the generated type information to JsonSerializer, such as AppJson.Default.Order.

    Other common generators in .NET:

    • Regular expressions compiled at build time with a generated-regex attribute.
    • High-performance logging methods generated from attributes.
    • Configuration binding and options validation.

    Trade-offs:

    • Generated code must be kept in sync by rebuilding, which happens automatically.
    • Not every scenario is supported, such as fully dynamic types.
    • Build times increase slightly.

    For services aiming at fast startup or ahead-of-time compilation, source-generated serialization is the recommended default.

    Example

    A generated serializer context handles the type without runtime reflection, including naming options.

    using System.Text.Json;
    using System.Text.Json.Serialization;
    
    var order = new Order(42, "paid", 1250.5m);
    string json = JsonSerializer.Serialize(order, AppJson.Default.Order);
    Console.WriteLine(json); // {"id":42,"status":"paid","total":1250.5}
    
    Order? back = JsonSerializer.Deserialize(json, AppJson.Default.Order);
    Console.WriteLine(back == order); // True
    
    record Order(int Id, string Status, decimal Total);
    
    [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
    [JsonSerializable(typeof(Order))]
    partial class AppJson : JsonSerializerContext;
  6. Q26.How do `async` and `await` work under the hood in C#?

    The compiler rewrites an async method into a state machine. At each await on an operation that has not finished, the method saves its local state, registers a continuation and returns an incomplete Task to its caller. When the operation completes, the continuation resumes the method from where it left off, usually on a thread pool thread.

    stateDiagram-v2
      state "Running until first await" as A
      state "Suspended, thread released" as B
      state "Resumed by continuation" as C
      state "Completed: task has result or exception" as D
      [*] --> A
      A --> B: Awaited task not finished
      A --> D: No incomplete awaits
      B --> C: Awaited task completes
      C --> B: Next await not finished
      C --> D: Method returns or throws

    What this means:

    1. No thread is blocked while waiting on input and output, which is why async services handle many concurrent requests with few threads.
    2. Exceptions are stored in the task and rethrown when it is awaited.
    3. Awaiting a completed task continues synchronously, without a thread switch.
    4. Async is not parallelism: it frees threads during waits; CPU-bound work still needs threads.

    Cost: each async call can allocate a state machine and a task when it actually suspends, so extremely hot paths sometimes use ValueTask or synchronous fast paths.

  7. Q27.What is the difference between `Task` and `ValueTask`?

    Both represent an asynchronous operation's result. Task is a class, so returning one usually allocates. ValueTask is a struct that can wrap either a result that is already available or a task, which avoids allocation when a method often completes synchronously, such as a cache hit.

    Task / Task<T>ValueTask / ValueTask<T>
    Reference type, usually allocated per callValue type, no allocation when the result is ready
    Can be awaited many times and stored freelyShould be awaited exactly once, and not stored
    Works with Task.WhenAll and other combinators directlyMust be converted with AsTask for combinators
    Best default for almost all methodsBest for hot paths that usually complete synchronously

    Rules for ValueTask:

    1. Await it once, immediately; awaiting twice or reading .Result before completion can corrupt pooled state.
    2. Do not use it just because it sounds faster; measure that allocations matter first.
    3. Interfaces that may be implemented with caching, such as stream reads, are good candidates.

    For ordinary application code, return Task and keep things simple.

  8. Q28.Why is blocking on asynchronous code with `.Result` or `.Wait()` dangerous?

    Blocking makes a thread wait for an asynchronous operation instead of releasing it. In a server, each blocked thread is a thread the pool cannot use, and if the awaited operation's continuation needs a thread to finish, requests can wait on each other until the service grinds to a halt.

    Problems it causes:

    • Thread pool starvation: under load, many requests block threads at once, the pool grows slowly and latency jumps from milliseconds to seconds.
    • Deadlocks in environments with a single-threaded synchronization context, such as some desktop frameworks and older web stacks, where the continuation waits for the thread that is blocked on it.
    • Wrapped exceptions: .Result and .Wait() throw AggregateException, hiding the original exception type.

    How to avoid it:

    1. Use async all the way up the call chain, from the endpoint to the database call.
    2. Use await instead of .Result, .Wait() or .GetAwaiter().GetResult().
    3. In rare places that must be synchronous, such as some constructors, move the async work into a factory method or startup code.
    4. Use analyzers that warn about blocking calls in async code.
  9. Q29.How do you run independent asynchronous operations concurrently in C#?

    Start all the operations first, then await them together with Task.WhenAll. Awaiting each one immediately after starting it runs them one after another, so the total time is the sum of the parts instead of the slowest part.

    Patterns:

    • Same result type: string[] results = await Task.WhenAll(tasks);, with results in the same order as the tasks.
    • Different result types: start tasks into variables, await Task.WhenAll(a, b), then read a.Result and b.Result, which are complete by then.
    • First to finish: Task.WhenAny returns the first completed task, useful for hedging or timeouts.
    • As each completes: Task.WhenEach yields tasks in completion order, so results can be processed as they arrive.

    Cautions:

    1. Make sure the operations are truly independent and safe to run at the same time.
    2. Limit concurrency for large collections, so you do not open thousands of connections at once.
    3. Pass the same cancellation token, so all of them stop when the request is cancelled.
    4. One failing task does not cancel the others automatically; combine with a linked cancellation source if needed.

    Example

    Three calls take about 450 ms when awaited one by one, but about 200 ms when awaited together.

    using System.Diagnostics;
    
    var sw = Stopwatch.StartNew();
    await Fetch("profile", 100);
    await Fetch("orders", 150);
    await Fetch("offers", 200);
    long sequential = sw.ElapsedMilliseconds;
    
    sw.Restart();
    string[] results = await Task.WhenAll(Fetch("profile", 100), Fetch("orders", 150), Fetch("offers", 200));
    long concurrent = sw.ElapsedMilliseconds;
    
    Console.WriteLine(string.Join(",", results)); // profile,orders,offers
    Console.WriteLine(sequential >= 400);          // True
    Console.WriteLine(concurrent < 350);           // True
    
    static async Task<string> Fetch(string name, int ms)
    {
        await Task.Delay(ms);
        return name;
    }
  10. Q30.How does cancellation with `CancellationToken` work in .NET?

    Cancellation is cooperative. A CancellationTokenSource creates a token and later signals it; code that receives the token checks it, or passes it to operations that support it, and stops by throwing OperationCanceledException. Nothing is aborted forcibly.

    How to use it:

    1. Accept a token as the last parameter of async methods, CancellationToken cancellationToken = default.
    2. Pass it down to every call that accepts one: database queries, HTTP requests, delays and stream reads.
    3. Check it in long loops with ThrowIfCancellationRequested.
    4. Combine signals with CancellationTokenSource.CreateLinkedTokenSource, for example the request's abort token plus a timeout.
    5. Dispose sources you create, especially those with timers.

    In web services, the framework supplies a token that is cancelled when the client disconnects, so passing it through stops wasted work.

    Handling cancellation:

    • Let OperationCanceledException propagate in most code.
    • At the top level, treat it as a normal outcome, not an error, so logs and alerts are not flooded when clients cancel.
    • Clean up in finally blocks, which run when cancellation exceptions unwind.

    Example

    A loop stops when its token is cancelled, and a linked source cancels when either of its parents does.

    using var cts = new CancellationTokenSource();
    Task work = PollAsync(cts.Token);
    await Task.Delay(120);
    cts.Cancel();
    
    try
    {
        await work;
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("polling stopped"); // polling stopped
    }
    
    using var request = new CancellationTokenSource();
    using var timeout = new CancellationTokenSource(TimeSpan.FromHours(1));
    using var linked = CancellationTokenSource.CreateLinkedTokenSource(request.Token, timeout.Token);
    request.Cancel();
    Console.WriteLine(linked.IsCancellationRequested); // True
    
    static async Task PollAsync(CancellationToken ct)
    {
        while (true)
        {
            await Task.Delay(50, ct);
        }
    }
  11. Q31.How do you add timeouts to asynchronous operations in .NET?

    Give the operation a cancellation token that cancels after a time limit, so the operation itself stops. Task.WaitAsync adds a timeout to waiting, but the underlying operation keeps running, so prefer real cancellation wherever the operation accepts a token.

    Options:

    • new CancellationTokenSource(timeout) or cts.CancelAfter(timeout), passed to the operation; it throws OperationCanceledException when the time runs out.
    • Linked sources combine a timeout with the caller's token.
    • await task.WaitAsync(timeout) throws TimeoutException if the task is too slow, which is useful for operations without cancellation support.
    • Client-level timeouts, such as HttpClient.Timeout, as an outer safety net.

    Setting the values:

    1. Base timeouts on measured latency, such as a little above the 99th percentile.
    2. Make inner timeouts shorter than outer ones, so the caller can still respond, perhaps with a fallback.
    3. Distinguish timeout from caller cancellation when logging: check whether the caller's token was cancelled.

    Example

    WaitAsync stops waiting, a timed token stops the operation itself and a fast call completes normally.

    try
    {
        await SlowAsync(500).WaitAsync(TimeSpan.FromMilliseconds(100));
    }
    catch (TimeoutException)
    {
        Console.WriteLine("wait timed out"); // wait timed out
    }
    
    using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
    try
    {
        await SlowAsync(500, cts.Token);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("operation cancelled"); // operation cancelled
    }
    
    Console.WriteLine(await SlowAsync(10).WaitAsync(TimeSpan.FromSeconds(1))); // done
    
    static async Task<string> SlowAsync(int ms, CancellationToken ct = default)
    {
        await Task.Delay(ms, ct);
        return "done";
    }
  12. Q32.What is thread pool starvation, and how do you diagnose it?

    Thread pool starvation happens when all thread pool threads are busy or blocked, so new work, including continuations of awaited operations, waits in a queue. The pool adds threads slowly, so latency rises sharply even though processor use may be low.

    flowchart TD
      A["Requests call blocking code such as .Result"] --> B["Pool threads are blocked waiting"]
      B --> C["Continuations queue up with no free thread"]
      C --> D["Pool adds threads slowly"]
      D --> E["Latency climbs from milliseconds to seconds"]
      E --> A

    Symptoms:

    • Latency rises under load while processor use stays moderate.
    • The thread count climbs steadily.
    • The thread pool queue length grows.

    How to diagnose:

    1. Watch runtime counters for thread pool thread count, queue length and completed work items.
    2. Take a memory dump or stack snapshot and look for many threads blocked in Wait, .Result, locks or synchronous input and output.
    3. Search the code for sync-over-async patterns and long synchronous calls in request paths.

    Fixes: make the call chain asynchronous, remove blocking waits, move CPU-heavy work off request threads with limits, and avoid raising the minimum thread count as a permanent fix, since it hides the cause.

  13. Q33.How does `ConfigureAwait` work, and when does it matter?

    By default, await captures the current synchronization context or task scheduler and resumes the method there. ConfigureAwait(false) tells the awaiter not to capture it, so the continuation can run on any thread pool thread.

    When it matters:

    • Desktop and older web frameworks have a single-threaded synchronization context; resuming on it is required for user interface code, but library code that blocks callers can deadlock if it captures the context.
    • Modern .NET web services have no synchronization context, so ConfigureAwait(false) has no effect on correctness there.

    Guidelines:

    1. General-purpose libraries that may be used in any application type should use ConfigureAwait(false) on their awaits, avoiding deadlocks and a small cost.
    2. Application code in modern web services can omit it.
    3. User interface code must resume on the user interface thread when it updates controls, so it should not use ConfigureAwait(false) before those updates.

    Related options: recent .NET versions accept ConfigureAwaitOptions, for example to suppress throwing when awaiting a task only to wait for completion.

  14. Q34.How do you protect shared state in C#: `lock`, `SemaphoreSlim` or `Interlocked`?

    Choose the lightest tool that protects the data correctly. Interlocked handles single numeric updates, lock protects short synchronous critical sections and SemaphoreSlim limits access when code inside the protected section must await.

    InterlockedlockSemaphoreSlim
    Atomic operations on one value: increment, exchange, compare-exchangeMutual exclusion for a block of synchronous codeLimits how many callers enter, and supports WaitAsync
    No blocking, very fastBlocks other threads brieflyCan await without blocking a thread
    Cannot protect several related fields togetherCannot contain awaitMust be released in finally
    Best for counters and flagsBest for updating several fields togetherBest for async critical sections and concurrency limits

    Guidelines:

    1. Keep critical sections short, with no input and output under a lock.
    2. Lock on a private object, or the newer System.Threading.Lock type, never on this or public objects.
    3. Release semaphores in finally, so exceptions cannot leak permits.
    4. Prefer immutable snapshots or concurrent collections for read-heavy data.
    5. Acquire multiple locks in a fixed order to avoid deadlocks.
  15. Q35.How do concurrent collections work, and what are the pitfalls of `ConcurrentDictionary`?

    Concurrent collections, such as ConcurrentDictionary, ConcurrentQueue and ConcurrentBag, are safe to use from many threads without external locks. Each individual operation is atomic, but sequences of operations are not.

    Pitfalls with ConcurrentDictionary:

    1. GetOrAdd factories can run more than once: if two threads miss the same key at the same time, both may run the factory, and one result is discarded. For expensive or side-effecting loads, store a Lazy<T>, or Lazy<Task<T>> for async loads, so only one load runs.
    2. Check-then-act races: if (!dict.ContainsKey(k)) dict[k] = v; is two operations; use TryAdd, GetOrAdd or AddOrUpdate.
    3. Values are not protected: storing a mutable List<T> as a value still needs its own synchronization.
    4. Enumeration is a moving snapshot: it does not block writers and may or may not include concurrent changes.
    5. Unbounded growth: it is not a cache; add expiry and size limits yourself.

    Choosing: ConcurrentQueue for producer-consumer queues, though channels are often better; ConcurrentDictionary for shared lookups with frequent concurrent updates; immutable or frozen collections for read-mostly data.

    Example

    Twenty concurrent requests for the same key trigger a single load, because the dictionary stores a lazy task.

    using System.Collections.Concurrent;
    
    var cache = new ConcurrentDictionary<string, Lazy<Task<string>>>();
    int loads = 0;
    
    Task<string> Get(string key) =>
        cache.GetOrAdd(key, k => new Lazy<Task<string>>(async () =>
        {
            Interlocked.Increment(ref loads);
            await Task.Delay(50);
            return $"value for {k}";
        })).Value;
    
    string[] results = await Task.WhenAll(Enumerable.Range(0, 20).Select(_ => Get("user:7")));
    
    Console.WriteLine(results.Distinct().Single()); // value for user:7
    Console.WriteLine(loads);                       // 1
  16. Q36.How do you build producer-consumer pipelines with `System.Threading.Channels`?

    A channel is an asynchronous queue: producers write items, consumers read them, and both sides can await without blocking threads. A bounded channel limits the number of buffered items, so fast producers wait for slow consumers instead of filling memory.

    flowchart LR
      A["Producers: requests or events"] -->|"WriteAsync"| B["Bounded channel"]
      B -->|"ReadAllAsync"| C["Consumer workers"]
      C --> D["Database or downstream service"]
      B -->|"Full: wait or drop"| A

    Key options:

    • Bounded or unbounded: prefer bounded with a capacity based on acceptable memory and delay.
    • Full mode: wait, drop the oldest, drop the newest or drop the incoming item.
    • Single reader or writer hints allow faster internal implementations.

    Pattern:

    1. Producers call await writer.WriteAsync(item, ct).
    2. When production is finished, call writer.Complete(), optionally with an exception.
    3. Consumers loop with await foreach (var item in reader.ReadAllAsync(ct)), which ends when the channel is completed and drained.

    Typical uses: background processing of incoming work, batching writes and decoupling request handling from slow downstream calls inside one process.

    Example

    A producer writes ten items into a channel of capacity five; the consumer reads until the channel completes.

    using System.Threading.Channels;
    
    var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(5)
    {
        FullMode = BoundedChannelFullMode.Wait,
    });
    
    Task producer = Task.Run(async () =>
    {
        for (int i = 1; i <= 10; i++)
            await channel.Writer.WriteAsync(i);
        channel.Writer.Complete();
    });
    
    int count = 0, sum = 0;
    await foreach (int item in channel.Reader.ReadAllAsync())
    {
        count++;
        sum += item;
    }
    await producer;
    Console.WriteLine($"{count} items, sum {sum}"); // 10 items, sum 55
  17. Q37.How do you limit the concurrency of asynchronous work in .NET?

    Cap how many operations run at once, so a large batch does not overwhelm a database, an external API or the process itself. .NET offers Parallel.ForEachAsync with a maximum degree of parallelism, and SemaphoreSlim for finer control.

    Parallel.ForEachAsync:

    • Processes a collection with at most MaxDegreeOfParallelism operations in flight.
    • Accepts a cancellation token, and stops scheduling new items when it is cancelled or an item throws.
    • Good for batch jobs, such as calling an API for every record.

    SemaphoreSlim:

    • await semaphore.WaitAsync(ct) before the operation and Release() in finally.
    • Good when calls come from many places, such as all requests sharing a limit on a downstream service.

    Choosing the limit:

    1. Base it on the bottleneck's capacity, such as the database pool size or the partner's rate limit, not on the number of cores.
    2. Add a timeout on waiting, so requests fail fast when the limit is saturated.
    3. Expose waiting time as a metric.

    Example

    Twelve items are processed with at most three in flight at any moment.

    int active = 0, peak = 0;
    
    await Parallel.ForEachAsync(
        Enumerable.Range(1, 12),
        new ParallelOptions { MaxDegreeOfParallelism = 3 },
        async (id, ct) =>
        {
            int now = Interlocked.Increment(ref active);
            RecordPeak(ref peak, now);
            await Task.Delay(20, ct);
            Interlocked.Decrement(ref active);
        });
    
    Console.WriteLine(peak); // 3
    
    static void RecordPeak(ref int peak, int value)
    {
        int current;
        while ((current = Volatile.Read(ref peak)) < value &&
               Interlocked.CompareExchange(ref peak, value, current) != current)
        {
        }
    }
  18. Q38.How does `IAsyncEnumerable<T>` stream data asynchronously?

    IAsyncEnumerable<T> represents a sequence whose items arrive asynchronously, such as pages from an API or rows from a database. An async iterator method uses await and yield return together, and consumers read it with await foreach, processing each item as it arrives instead of waiting for the whole collection.

    Benefits:

    • Low memory: only the current item, or page, is held at a time.
    • Early results: processing starts as soon as the first item is available.
    • Natural backpressure: the producer runs only as fast as the consumer asks for items.

    Cancellation: mark the iterator's token parameter with [EnumeratorCancellation], so consumers can pass a token with WithCancellation(ct), and the iterator stops cleanly.

    Common uses:

    1. Reading large query results without loading them all.
    2. Following paginated APIs, hiding paging from callers.
    3. Streaming JSON arrays from web endpoints; the framework writes items as they are produced.

    Caution: the sequence runs again each time it is enumerated, like other lazy sequences, so materialize it if you need it twice.

    Example

    Pages are produced one at a time, and the consumer stops early after two of them.

    using System.Runtime.CompilerServices;
    
    await foreach (string page in FetchPagesAsync(5))
    {
        Console.WriteLine(page);
        if (page == "page 2") break;
    }
    // Output:
    // fetching page 1
    // page 1
    // fetching page 2
    // page 2
    
    static async IAsyncEnumerable<string> FetchPagesAsync(int pages, [EnumeratorCancellation] CancellationToken ct = default)
    {
        for (int i = 1; i <= pages; i++)
        {
            Console.WriteLine($"fetching page {i}");
            await Task.Delay(10, ct);
            yield return $"page {i}";
        }
    }

    Page 3 is never fetched, because the consumer stopped asking.

  19. Q39.What is `async void`, and why should you avoid it?

    An async void method returns nothing that callers can await. Its exceptions cannot be caught by the caller; they are raised on the synchronization context or the thread pool, which in a service usually crashes the process. Callers also cannot know when it finishes.

    Why it is dangerous:

    1. Unobservable failures: a try/catch around the call does not catch its exceptions.
    2. No completion signal: tests and shutdown logic cannot wait for it.
    3. Accidental use: lambdas passed where an Action is expected silently become async void, for example in list.ForEach(async x => await SaveAsync(x)).

    The one legitimate use: event handlers whose signature requires void, such as user interface events. Even then, wrap the body in try/catch and log failures.

    Alternatives:

    • Return Task from every async method, and await it.
    • For fire-and-forget background work, hand items to a background service through a channel, so failures are logged and shutdown waits for in-flight work.
    • Enable analyzer rules that flag async void and async lambdas converted to Action.
  20. Q40.How do you handle exceptions from several concurrent tasks?

    await Task.WhenAll(...) rethrows only the first exception, but the combined task keeps all of them. To see every failure, keep a reference to the WhenAll task and read its Exception.InnerExceptions after catching.

    Behavior to know:

    • All tasks run to completion even if one fails; WhenAll does not cancel the others.
    • Only one exception is rethrown by await, the first one stored in the combined task; do not rely on which one that is when several tasks fail together.
    • Cancelled tasks make the combined task cancelled if no task failed.

    Patterns:

    1. Report all failures: catch, then iterate over whenAllTask.Exception!.InnerExceptions.
    2. Fail fast and cancel the rest: share a linked CancellationTokenSource, and cancel it in each task's catch before rethrowing.
    3. Collect results and errors per item: wrap each operation to return a result type with either a value or an error, so partial success is easy to report.

    Unobserved exceptions: a task that fails and is never awaited raises an event later and loses the error silently in most configurations, so always await or observe tasks you start.

    Example

    Awaiting rethrows one failure, while the combined task still holds both; different delays make the order predictable here.

    Task all = Task.WhenAll(Fail("inventory", 10), Fail("pricing", 60), Succeed());
    try
    {
        await all;
    }
    catch (Exception e)
    {
        Console.WriteLine($"await rethrew: {e.Message}");
        Console.WriteLine($"all errors: {string.Join(", ", all.Exception!.InnerExceptions.Select(x => x.Message))}");
    }
    // Output:
    // await rethrew: inventory failed
    // all errors: inventory failed, pricing failed
    
    static async Task Fail(string name, int ms)
    {
        await Task.Delay(ms);
        throw new InvalidOperationException($"{name} failed");
    }
    
    static async Task Succeed() => await Task.Delay(5);