.NET 8+ — all questions in one page
1011 questions and answers on .NET 8 or later, C# and REST API development, numbered continuously and searchable: the C# language, async and concurrency, dependency injection, ASP.NET Core and API design, EF Core and data access, performance and diagnostics, testing, observability, security, modern .NET and architecture. Search jumps straight to a question; the buttons filter by level or show the copy-usable building-block recipes.
Loading index…
No matches. Try another keyword — the search covers the question and its answer.
Beginner 280 questions
C# language fundamentals: types, strings, collections, LINQ, exceptions, tooling and the basics of the runtime.
1. What is the difference between a value type and a reference type in C#?
A value type stores its data directly where the variable lives, so assignment copies the whole value. A reference type stores a reference on the stack or inside another object, and the actual object lives on the heap, so assignment copies only the reference. Structs, enums and the built-in numeric types are value types, while classes, arrays, delegates and strings are reference types.
int a = 10;
int b = a; // copy
b = 20; // a is still 10
var list1 = new List<int> { 1 };
var list2 = list1; // same object
list2.Add(2); // list1.Count == 2
2. Which built-in C# types are value types and which are reference types?
All numeric types (int, long, double, decimal, byte, etc.), bool, char, DateTime, TimeSpan, enums and structs are value types. string, object, arrays, classes, interfaces and delegates are reference types. Note that string looks like a value type because of its immutability, but it is a reference type.
3. Why is string immutable in .NET and what does that imply?
Once a string instance is created its contents can never change; every "modification" such as Replace or += creates a new string. This makes strings safe to share across threads and allows the runtime to cache hash codes and intern literals. The practical consequence is that repeated concatenation in a loop allocates many temporary objects.
4. When should you use StringBuilder instead of string concatenation?
Use StringBuilder when you build a string from many pieces, especially inside a loop or when the final size is unknown, because it mutates an internal buffer instead of allocating a new string each time. For a handful of concatenations the compiler-generated String.Concat is faster and clearer. Call ToString once at the end.
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i).Append(',');
}
string result = sb.ToString();
5. What is the difference between an array and a List<T>?
An array has a fixed length decided at creation and cannot grow, while List<T> wraps an array and resizes itself by allocating a larger buffer when capacity is exceeded. Arrays are slightly faster and allocation-friendly for fixed-size data; lists are the default choice when the count changes. Both allow index access and are reference types.
6. How do you declare and initialize a jagged array versus a multidimensional array?
A multidimensional array uses a single block with comma-separated indices, while a jagged array is an array of arrays where each inner array can have a different length. Access syntax differs: matrix[1, 2] versus jagged[1][2]. Jagged arrays are usually more flexible and sometimes faster for uneven data.
int[,] grid = new int[2, 3];
int[][] jagged = new int[2][];
jagged[0] = new[] { 1, 2 };
jagged[1] = new[] { 3, 4, 5 };
7. What does the params keyword do in a method signature?
params lets the caller pass a variable number of arguments of the specified type without explicitly creating an array. The compiler packs the arguments into an array, and the parameter must be the last one in the signature. It is convenient for logging or formatting helpers but allocates an array on each call.
static int Sum(params int[] values)
{
int total = 0;
foreach (var v in values) total += v;
return total;
}
int x = Sum(1, 2, 3);
8. How do optional and named arguments work together?
An optional parameter declares a default value and may be omitted by the caller; optional parameters must come after all required ones. Named arguments let you pass a value by parameter name in any order, which improves readability when several parameters share a type. You can combine both, for example skipping one optional argument while supplying a later one.
void Log(string message, string level = "INFO", bool console = true) { }
Log("started");
Log("failed", console: false);
9. What is the difference between ref, out and in parameters?
ref requires the variable to be initialized before the call and allows the method to read and write it. out requires no prior initialization but the method must assign it before returning, which suits TryParse-style APIs. in passes by reference for performance but forbids modification, so it is used for large structs that should not be copied.
static void Increment(ref int value) => value++;
static bool TryHalf(int input, out int half)
{
half = input / 2;
return input % 2 == 0;
}
10. What is a nullable value type and how is it represented?
Nullable<T>, written T?, wraps a value type so it can also represent the absence of a value. It is a struct with two members, HasValue and Value, and Value throws InvalidOperationException when HasValue is false. This is how a database NULL is mapped onto types like int or DateTime.
int? count = null;
if (count.HasValue)
Console.WriteLine(count.Value);
int safe = count ?? 0; // null-coalescing
11. How do you safely read the value of a nullable type?
Check HasValue before touching Value, use GetValueOrDefault to fall back to default(T), or use the null-coalescing operator to supply an alternative. Pattern matching with "is int value" is also safe because it only succeeds when a value exists. Never access Value directly on a null nullable.
12. What is the difference between an enum and an enum with the [Flags] attribute?
A plain enum represents exactly one named constant, whereas a flags enum is designed to combine several values with bitwise OR. For flags you assign powers of two so each bit is unique, and you mark the type with [Flags] so ToString prints combined names. Membership is then tested with HasFlag or a bitwise AND.
[Flags]
enum Permissions { None = 0, Read = 1, Write = 2, Delete = 4 }
var p = Permissions.Read | Permissions.Write;
bool canWrite = p.HasFlag(Permissions.Write);
13. When would you choose a struct over a class?
Choose a struct for small, immutable data that logically behaves like a single value, such as a coordinate or a money amount, because it avoids heap allocation and garbage collection. Keep it under roughly 16-24 bytes, since larger structs get copied expensively on every assignment or argument pass. If you need inheritance, reference semantics or identity, use a class.
14. What are the main differences between structs and classes in C#?
Structs are value types stored inline and copied on assignment, classes are reference types stored on the heap. Structs cannot inherit from another struct or class (they implicitly derive from ValueType) and cannot have a parameterless constructor with custom logic in older language versions. Structs also cannot be null unless wrapped in Nullable<T>.
15. What does a static constructor do and when does it run?
A static constructor initializes static fields and runs automatically at most once, before the first static member access or the first instance creation. It cannot be called directly and takes no parameters or access modifiers. If it throws, the type becomes unusable for the rest of the application domain.
class Config
{
public static readonly string Root;
static Config() => Root = AppContext.BaseDirectory;
}
16. What is constructor chaining with this() and base()?
this(...) calls another constructor in the same class so shared initialization lives in one place, while base(...) forwards arguments to a base class constructor. Both must appear in the constructor initializer before the body runs. If you omit base(...), the compiler inserts a call to the parameterless base constructor.
class Person
{
public string Name { get; }
public Person() : this("unknown") { }
public Person(string name) => Name = name;
}
17. What is the difference between a field and a property?
A field is a raw storage location exposed directly, while a property is a member with get and/or set accessors that can add validation, lazy loading, logging or computed logic. Properties are the public API convention because you can change their implementation without breaking callers. Auto-properties let the compiler generate the backing field for you.
private int _age;
public int Age
{
get => _age;
set => _age = value < 0 ? 0 : value;
}
18. How do you create and use an indexer?
An indexer is a property named this with parameters in brackets, letting instances be accessed with array-like syntax. It can have any parameter type and can be overloaded, and it may be read-only or read-write. Indexers are common on collection wrappers.
class Bag
{
private readonly List<string> _items = new();
public string this[int i] => _items[i];
public void Add(string s) => _items.Add(s);
}
19. What do the access modifiers public, private, protected, internal and protected internal mean?
public is visible everywhere, private only inside the declaring type, protected to derived types, and internal only within the same assembly. protected internal is the union of protected and internal, so either condition grants access. private protected, newer and rarer, requires both derived and same assembly.
20. What is the difference between const, readonly and static readonly?
const is a compile-time constant baked into every consuming assembly and limited to primitives, string and null. readonly is a runtime field that can be assigned only in the declaration or a constructor, so each instance may differ. static readonly combines both ideas: one value per type, assigned at runtime, which suits values from configuration or DateTime.Now.
21. What is the difference between var, object and dynamic?
var is compile-time type inference: the variable is strongly typed and the compiler knows its exact type. object is a static type that forces boxing of value types and requires casts to use members. dynamic defers binding to runtime, so mistakes become runtime exceptions rather than compile errors and IntelliSense is lost.
var name = "Ann"; // string
object boxed = 42; // needs unboxing
dynamic d = 42;
d = "now a string"; // allowed, checked at runtime
22. What is the difference between implicit and explicit conversion?
An implicit conversion happens automatically because it is always safe, such as int to long or a derived class to its base. An explicit conversion (a cast) is required when data could be lost or the conversion could fail, such as double to int. Custom types can define both with implicit and explicit operator methods.
23. What is the difference between the is and as operators?
is tests compatibility and returns a bool without throwing, and in modern C# it can also declare a pattern variable in the same expression. as performs a reference conversion and returns null instead of throwing when it fails, so it only works with reference or nullable types. Neither performs user-defined conversions.
if (obj is string s && s.Length > 3)
Console.WriteLine(s);
var str = obj as string; // null if not a string
24. What happens when you cast an incompatible reference type?
An explicit cast between incompatible reference types throws InvalidCastException at runtime, whereas a failed as returns null. This is why testing with is before casting, or using a pattern, is safer than blind casts. Casting to a base or interface type never fails if the object really implements it.
25. How does basic pattern matching simplify type checks?
Pattern matching combines the test and the extraction in one expression, removing duplicated casts. A type pattern declares a variable only in scope when the match succeeds, and a switch expression can map several shapes to results concisely. Property, constant and relational patterns extend this further.
string Describe(object o) => o switch
{
int n when n < 0 => "negative",
int n => $"int {n}",
string s => $"string of length {s.Length}",
null => "null",
_ => "other"
};
26. What is a tuple and how do you deconstruct one?
A tuple groups a fixed number of values with lightweight syntax and no dedicated type. You deconstruct it by assigning its elements to separate variables in one statement, or by using the var (a, b) form. Tuples are useful for returning multiple values without defining a class.
(int min, int max) Range(int[] data)
=> (data.Min(), data.Max());
var (low, high) = Range(new[] { 3, 9, 1 });
Console.WriteLine($"{low}-{high}");
27. What is the difference between ValueTuple and the older Tuple class?
ValueTuple is a mutable value type with named element support and no heap allocation, and it is what the (a, b) syntax compiles to. The older System.Tuple is a reference type with read-only properties named Item1, Item2 and so on. Prefer ValueTuple unless you specifically need a class.
28. What is the difference between List<T>, Dictionary<TKey,TValue> and HashSet<T>?
List<T> is an ordered indexable sequence allowing duplicates. Dictionary<TKey,TValue> maps unique keys to values with near-constant lookup by key, and HashSet<T> stores unique elements with fast membership tests. Choose based on whether you need ordering, key lookup, or uniqueness.
29. What is the difference between Queue<T> and Stack<T>?
Queue<T> is first-in-first-out: Enqueue adds at the back and Dequeue removes from the front. Stack<T> is last-in-first-out: Push adds on top and Pop removes the most recent item. Use Peek to inspect the next item without removing it.
var q = new Queue<int>();
q.Enqueue(1); q.Enqueue(2);
int first = q.Dequeue(); // 1
var s = new Stack<int>();
s.Push(1); s.Push(2);
int top = s.Pop(); // 2
30. What is the time complexity of a Dictionary lookup and why?
Dictionary lookup is O(1) on average because it hashes the key and indexes directly into a bucket array. Worst case degrades to O(n) if many keys collide, which is why keys need a good GetHashCode implementation. Enumeration order is not guaranteed.
31. How do you implement a custom iterator with yield return?
A method returning IEnumerable<T> can use yield return to produce elements lazily, one at a time, without building a collection. The compiler turns the method into a state machine, so execution pauses at each yield and resumes on the next MoveNext. This makes infinite or streaming sequences possible.
static IEnumerable<int> Evens(int limit)
{
for (int i = 0; i <= limit; i += 2)
yield return i;
}
foreach (var n in Evens(6)) Console.WriteLine(n);
32. What does the using statement guarantee and what pattern does it rely on?
The using statement guarantees that Dispose is called when the block exits, even if an exception is thrown. It works with any type implementing IDisposable, and a using declaration (without braces) disposes at the end of the enclosing scope. This is how files, connections and locks are released deterministically.
using var stream = File.OpenRead("data.txt");
using var reader = new StreamReader(stream);
string content = reader.ReadToEnd();
33. What is the difference between throw and throw ex inside a catch block?
throw on its own rethrows the current exception and preserves the original stack trace, while throw ex resets the stack trace to the current line, hiding where the error really started. Always prefer bare throw unless you intentionally want to hide frames. Wrapping is better done with throw new Exception("context", ex).
34. When should you catch an exception rather than let it propagate?
Catch only when you can actually handle the failure: retry, fall back, translate to a domain error, or add context. Catching Exception silently or using exceptions for normal control flow hides bugs and hurts performance. Let unexpected errors bubble up to a global handler that logs them.
35. What is the purpose of the finally block?
The finally block runs whether the try block completed normally, threw, or returned, which makes it suitable for cleanup that must always happen. It executes after any catch blocks. With using statements most cleanup is already handled, but finally is still useful for flags or metrics.
36. What is a namespace and how do using directives work?
A namespace is a logical container that groups related types and prevents name collisions; it does not affect assembly boundaries. A using directive imports a namespace so type names can be written unqualified, and a using alias can shorten or disambiguate a name. Global usings and implicit usings reduce boilerplate in modern projects.
using System.Text;
using Json = System.Text.Json.JsonSerializer;
37. What is the difference between DateTime, DateTimeOffset and TimeSpan?
DateTime holds a date and time with an ambiguous Kind (Utc, Local or Unspecified), DateTimeOffset adds an explicit offset from UTC, and TimeSpan represents a duration rather than a point in time. Prefer DateTimeOffset for values that cross time zones or get persisted. Use TimeSpan for elapsed time and intervals.
38. How should you store and compare timestamps correctly?
Store timestamps in UTC (DateTime.UtcNow or DateTimeOffset.UtcNow) and convert to local time only for display. Compare DateTimeOffset values rather than DateTime with mixed kinds, because comparison of Unspecified values ignores offsets. TimeZoneInfo handles conversions when a specific zone is needed.
39. How do you format a number or date with a custom format string?
Standard format specifiers like "C", "N2" or "yyyy-MM-dd" describe the output, and they can be passed to ToString or used inside interpolation. Custom patterns use placeholder characters such as dd, MM, HH and mm. Culture affects separators, currency symbols and month names.
decimal price = 1234.5m;
Console.WriteLine(price.ToString("C2")); // culture aware
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm"));
40. What does string interpolation do and how does it differ from string.Format?
Interpolation embeds expressions directly in a string literal prefixed with $, and the compiler turns it into a format call or a builder. It is more readable and compile-checked than positional placeholders, since a wrong expression fails at compile time. You can add alignment and format specifiers after a colon.
string name = "Ann";
int score = 7;
Console.WriteLine($"{name,-10}|{score,5:N1}");
41. Why does culture matter when parsing or formatting strings?
Different cultures use different decimal separators, date orders and currency symbols, so "1,5" may parse as 15 or 1.5 depending on culture. Use CultureInfo.InvariantCulture for machine-readable formats such as JSON, logs or file names, and the current culture for user-facing text. Always be explicit when the input source is unknown.
double v = double.Parse("1.5", CultureInfo.InvariantCulture);
string s = v.ToString("0.00", CultureInfo.InvariantCulture);
42. What is the difference between Math.Round, Math.Floor and Math.Ceiling?
Floor always moves down to the nearest integer, Ceiling always moves up, and Round goes to the nearest value using banker's rounding by default. That default means 2.5 rounds to 2, not 3, which surprises many developers. Pass MidpointRounding.AwayFromZero when you expect school-style rounding.
43. How do you read a text file line by line in C#?
File.ReadLines returns a lazy sequence that streams the file, so it does not load everything into memory, whereas File.ReadAllLines materializes an array. Wrap stream access in using so the handle is released promptly. Use async variants in I/O-bound code.
foreach (string line in File.ReadLines("log.txt"))
{
if (line.Contains("ERROR"))
Console.WriteLine(line);
}
44. How do you write text to a file and ensure the handle is closed?
File.WriteAllText is the simplest option for small content and closes the file itself. For larger or incremental writes, open a StreamWriter inside a using block so buffered data is flushed and the handle released. Consider passing an encoding explicitly when non-ASCII text matters.
45. What is the difference between File and FileInfo?
File is a static helper with one-shot methods like Exists, ReadAllText and Delete, while FileInfo is an instance that caches metadata and exposes properties such as Length and LastWriteTime. Use FileInfo when you perform several operations on the same path. Both ultimately call the same underlying APIs.
46. How do you serialize an object to JSON with System.Text.Json?
JsonSerializer.Serialize converts an object graph to a JSON string using public properties by default, and Deserialize reverses it. Property names follow the C# names unless you configure a naming policy such as camelCase. Options are passed as a JsonSerializerOptions instance, ideally cached and reused.
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
string json = JsonSerializer.Serialize(person, options);
47. How do you handle a JSON property whose name differs from the C# property?
Apply the [JsonPropertyName] attribute to map an external name onto the property, which is common when consuming APIs that use snake_case. Unknown members are ignored by default, and missing members leave the property at its default value. Mark required members if their absence should fail deserialization.
class User
{
[JsonPropertyName("user_name")]
public string UserName { get; set; } = "";
}
48. What does async/await actually do?
async marks a method that may suspend, and await releases the current thread while waiting for an incomplete task, resuming the continuation when the operation finishes. It is not about creating threads; it is about not blocking one. The compiler rewrites the method into a state machine and returns a Task or Task<T>.
static async Task<string> LoadAsync(string path)
{
string text = await File.ReadAllTextAsync(path);
return text.Trim();
}
49. Why is calling .Result or .Wait() on a task dangerous?
Blocking on an async operation can deadlock in contexts with a synchronization context, and it ties up a thread that async was meant to free. It also wraps failures in AggregateException, hiding the real error. Make the call chain async all the way, or use GetAwaiter().GetResult() only as a last resort at the very top.
50. What is the difference between Task and Task<T>?
Task represents an operation that completes without producing a value, while Task<T> produces a result of type T accessible through await or Result. Both carry completion state and exceptions. Use ValueTask<T> only when a hot path frequently completes synchronously.
51. What are top-level statements and when are they appropriate?
Top-level statements let you write executable code directly in a .cs file without an explicit Main method; the compiler generates the entry point. They suit small console apps, scripts and samples, keeping boilerplate low. Larger applications should still use explicit classes for structure and testability.
// Program.cs
var names = new[] { "Ann", "Bob" };
foreach (var n in names)
Console.WriteLine($"Hello {n}");
52. How does operator precedence affect the result of an expression?
Operators with higher precedence bind tighter, so multiplication happens before addition and assignment comes last. When two operators share precedence, associativity decides the order, usually left to right. Parentheses override everything and are the clearest way to express intent.
int a = 2 + 3 * 4; // 14
int b = (2 + 3) * 4; // 20
bool c = !true || true; // true
53. What is the difference between && and & in a boolean expression?
&& short-circuits: the right operand is not evaluated when the left is false, and || skips the right when the left is true. The single-character & and | are non-short-circuiting logical or bitwise operators and always evaluate both sides. Prefer the short-circuit forms for null checks.
if (list != null && list.Count > 0) { } // safe
// if (list != null & list.Count > 0) would throw
54. What do the null-coalescing operators ?? and ??= do?
?? returns the left operand if it is not null, otherwise the right operand, which makes it a compact fallback. ??= assigns the right value only when the left variable is null. Both avoid verbose if-null blocks and work with nullable value and reference types.
string name = input ?? "guest";
cache ??= new Dictionary<string, int>();
55. What is the difference between the prefix and postfix increment operators?
++x increments first and then yields the new value, while x++ yields the current value and increments afterwards. The difference is invisible when the result is discarded, but matters inside larger expressions. Mixing increments into complex expressions hurts readability and is best avoided.
int x = 5;
int a = ++x; // x=6, a=6
int b = x++; // b=6, x=7
56. How does the conditional operator ?: work and when should you use it?
The ternary operator evaluates a condition and returns one of two expressions, both of which must be compatible with the target type. It works well for short, side-effect-free choices and can replace a small if/else that only assigns a value. Avoid nesting it deeply, since readability drops quickly.
string label = count == 1 ? "item" : "items";
57. What is the difference between break and continue inside a loop?
break exits the loop immediately, while continue skips the rest of the current iteration and moves to the next one. Neither affects code after the loop; break jumps there directly. In nested loops both act only on the innermost loop.
58. Does a C# switch statement allow fall-through between cases?
No, C# forbids implicit fall-through: each non-empty section must end with break, return, throw, continue or goto, so no execution leaks into the next case. Multiple case labels may share one section, which is the supported way to group values. goto case or goto default can be used for explicit jumps, though it is rarely a good idea.
switch (code)
{
case 0:
case 1:
Console.WriteLine("low");
break;
default:
Console.WriteLine("high");
break;
}
59. What is a switch expression and how does it differ from a switch statement?
A switch expression produces a value, uses arrow arms instead of case labels, and requires an exhaustive set of arms (often with a discard _). It is more compact and composes well inside assignments. A switch statement is still appropriate when each branch performs statements or has side effects.
string Size(int n) => n switch
{
< 0 => "invalid",
0 => "empty",
<= 10 => "small",
_ => "large"
};
60. When would you use foreach instead of a for loop?
foreach is the clearest choice when you only need each element and do not modify the collection structure, because it works with any IEnumerable and hides index bookkeeping. A for loop is needed when you require the index, iterate backwards, skip elements, or mutate array elements in place. Modifying a collection during foreach throws InvalidOperationException for most collections.
61. What is the difference between a while loop and a do-while loop?
while checks the condition before each iteration, so the body may never run. do-while checks after the body, guaranteeing at least one execution, which suits input validation loops. Both need something inside to change the condition or they loop forever.
string input;
do
{
input = Console.ReadLine();
} while (string.IsNullOrWhiteSpace(input));
62. How do you exit a nested loop cleanly?
break only leaves the innermost loop, so the usual clean solutions are a flag checked by the outer loop, a return from a helper method, or a goto label placed after both loops. Extracting the search into a method with return is generally the most readable option.
static bool Contains(int[,] grid, int target)
{
foreach (var v in grid)
if (v == target) return true;
return false;
}
63. What is boxing and unboxing?
Boxing wraps a value type in a heap object when it is assigned to object or an interface it implements; unboxing extracts the value back with an explicit cast. Both cost allocations and copies, so they hurt performance in tight loops. Generic collections avoid boxing because they store T directly.
object boxed = 42; // boxing
int value = (int)boxed; // unboxing
64. What is the difference between a cast, Convert.ToInt32, int.Parse and int.TryParse?
A cast requires the source to already be a numeric type and does no string conversion, while int.Parse converts a string and throws FormatException on bad input. Convert.ToInt32 accepts strings and nulls, returning 0 for null, and int.TryParse returns false instead of throwing. TryParse is the safe default for user input.
if (int.TryParse(input, out int qty))
Console.WriteLine(qty * 2);
else
Console.WriteLine("not a number");
65. What happens on integer overflow and how do checked and unchecked change it?
By default arithmetic overflow in unchecked context silently wraps around, producing a wrong but valid value. The checked keyword or a checked block makes overflow throw OverflowException instead. Compiler settings can make the whole project checked by default.
int max = int.MaxValue;
int wrapped = max + 1; // int.MinValue
// int error = checked(max + 1); // OverflowException
66. When should you use decimal instead of double?
decimal is a base-10 type with high precision and is the right choice for money and other exact decimal quantities. double is binary floating point, faster and wider in range, but cannot represent values like 0.1 exactly. Comparisons of double values should use a tolerance rather than equality.
double d = 0.1 + 0.2; // 0.30000000000000004
decimal m = 0.1m + 0.2m; // 0.3
67. What is the difference between a char and a string containing one character?
char is a 16-bit value type holding a single UTF-16 code unit and uses single quotes, while string is an immutable reference type that may hold any number of characters and uses double quotes. A char can be compared and used in arithmetic through its numeric value. Converting between them uses ToString or the string indexer.
68. How should you compare two strings correctly?
== on strings performs an ordinal value comparison, which is usually what you want for equality. For explicit intent and culture control use string.Equals with a StringComparison value, and use Compare or string.Compare for ordering. Avoid ToLower comparisons, which allocate and behave oddly for some cultures.
bool same = string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
int order = string.Compare(a, b, StringComparison.Ordinal);
69. What is the difference between string.Empty, "" and null?
string.Empty and "" are both non-null empty strings (and Empty is the same interned instance as the literal in practice), while null means no string object at all. Calling a member on null throws NullReferenceException, so validate with string.IsNullOrEmpty or IsNullOrWhiteSpace. Prefer string.Empty over "" only for style consistency.
70. How do you split a string and skip empty entries?
string.Split accepts a StringSplitOptions value that removes empty results and can also trim whitespace around entries. Without the option, consecutive separators produce empty strings. The overload taking char[] plus options covers most parsing needs.
var parts = csv.Split(',', StringSplitOptions.RemoveEmptyEntries
| StringSplitOptions.TrimEntries);
71. What is a verbatim string literal and when do you use it?
A verbatim literal prefixed with @ treats backslashes literally and allows newlines, so file paths and regex patterns stay readable. To include a double quote inside it, double the quote. Interpolation can be combined as $@ or @$.
string path = @"C:\logs\app.log";
string quoted = @"He said ""hi""";
72. What members does a derived class inherit from its base class?
A derived class inherits all accessible members: public and protected fields, properties, methods and nested types, but not constructors or private members. Constructors must be chained explicitly with base(...) when arguments are needed. Inheritance expresses an is-a relationship and enables substitution through the base type.
73. What is the difference between virtual/override and new?
A virtual member can be replaced by an override in a derived class, and calls through the base reference dispatch to the override at runtime. The new keyword hides a base member instead, so the behavior depends on the static type of the reference. Hiding is a common source of subtle bugs; override is usually what you want.
class A { public virtual string Name => "A"; }
class B : A { public override string Name => "B"; }
A obj = new B();
Console.WriteLine(obj.Name); // B
74. What is the difference between an abstract class and an interface?
An abstract class can hold state, constructors and implemented members, and a class may inherit only one of them, so it suits a shared partial implementation. An interface declares a contract and a class can implement many, which favors decoupling and testability. Modern C# allows default interface members, but interfaces still cannot hold instance state.
75. What does the sealed keyword do?
A sealed class cannot be inherited, and a sealed override prevents further overriding of that member in derived classes. Sealing is useful for security-sensitive or performance-critical types and communicates that the design is final. Sealed classes are also slightly cheaper for some runtime checks.
76. In what order do constructors run in an inheritance hierarchy?
Base class field initializers and the base constructor run first, then the derived field initializers, then the derived constructor body. That is why a virtual call from a base constructor reaches an override whose fields are not yet initialized. Constructor execution therefore goes from the most general to the most specific type.
77. What is polymorphism and how does C# implement runtime polymorphism?
Polymorphism lets one reference type express several concrete behaviors, so callers depend on an abstraction instead of a specific class. C# implements runtime polymorphism through virtual methods and interface dispatch, resolved via the object's actual type. Compile-time polymorphism comes from overload resolution and generics.
78. Can an interface contain an implementation in modern C#?
Yes, since C# 8 interfaces may declare default implementations for members, which lets library authors add members without breaking implementers. Default members are reached through the interface reference, not through the class unless the class redeclares them. Interfaces still cannot declare instance fields or constructors.
79. What is explicit interface implementation and why use it?
Explicit implementation names the member as InterfaceName.Member, so it is only callable through that interface reference and is not part of the class's public surface. It resolves name collisions when two interfaces declare the same member and keeps the API clean. A common example is IEnumerable.GetEnumerator.
class Repo : IDisposable
{
void IDisposable.Dispose() { /* cleanup */ }
}
80. How do you decide between an interface and an abstract base class?
Use an interface when unrelated types should share a capability and you want multiple inheritance of contract, or when you need easy mocking in tests. Use an abstract class when implementations share state or common logic that should not be duplicated. Many designs combine both: an abstract class implementing an interface.
81. What is a partial class used for?
partial splits one type's definition across several files that the compiler merges, which suits generated code (designers, source generators) separated from hand-written code. All parts must share the same namespace, name and accessibility. Partial methods allow the generated part to declare a hook that the manual part may implement.
// Order.generated.cs
public partial class Order { public int Id { get; set; } }
// Order.cs
public partial class Order { public decimal Total() => 0m; }
82. What are partial methods and what are their constraints?
A partial method is declared in one part of a partial type, usually by generated code, and optionally implemented in another part. If no implementation exists and the method returns void, the compiler removes the call entirely. Newer versions allow return values and accessibility, but then an implementation becomes mandatory.
83. What is a record and what makes it different from a class?
A record is a reference type with compiler-generated value equality, a readable ToString, and support for non-destructive mutation via with. Equality compares all positional and init-only properties rather than object identity. Records are ideal for DTOs, messages and domain values that are defined by their data.
public record Person(string Name, int Age);
var a = new Person("Ann", 30);
var b = a with { Age = 31 };
Console.WriteLine(a == new Person("Ann", 30)); // True
84. What is the difference between a record class and a record struct?
A record class is a reference type with value-based equality, while a record struct is a value type that also gets value equality plus a mutable or readonly layout. record struct avoids heap allocation for small data but is copied on assignment. Both support positional syntax and with expressions.
85. What does the with expression do on a record?
with creates a shallow copy of a record and applies the listed changes, leaving the original untouched. This makes immutable update patterns cheap to express. Because the copy is shallow, nested reference members are still shared between the two instances.
var original = new Person("Ann", 30);
var older = original with { Age = 31 };
86. What are the requirements for defining an extension method?
An extension method must be declared in a static non-generic (or generic) static class, be static itself, and have this as the first parameter modifier. The class must be in scope via a using directive. Extension methods cannot access private members and are not virtual.
public static class StringExtensions
{
public static bool IsBlank(this string? s)
=> string.IsNullOrWhiteSpace(s);
}
bool blank = " ".IsBlank();
87. If an extension method and an instance method have the same signature, which wins?
The instance method always wins; extension methods are only considered when no applicable instance member exists. This means adding a method to a type later can silently change which code runs. Extension methods also cannot override virtual members.
88. What does deferred execution mean in LINQ?
A LINQ query over IEnumerable is not executed when it is defined; it runs when you enumerate it, and it re-runs on every enumeration. This allows composition and streaming but can cause repeated database or file work. Materialize with ToList, ToArray or a count when you need a stable snapshot.
var query = numbers.Where(n => n > 2); // nothing happens
var list = query.ToList(); // executes now
89. What is the difference between Where and Select?
Where filters elements and keeps the original type, while Select projects each element into a new shape or type. Both are lazy and can be chained in any order, though filtering first usually does less work. Select is the equivalent of map in functional terms.
var names = people
.Where(p => p.Age >= 18)
.Select(p => p.Name)
.ToList();
90. How do OrderBy, ThenBy and OrderByDescending work together?
OrderBy establishes the primary sort and returns an IOrderedEnumerable, on which ThenBy adds secondary keys. Calling OrderBy a second time replaces the previous ordering instead of refining it. Use OrderByDescending for a descending primary key.
var sorted = people
.OrderBy(p => p.LastName)
.ThenByDescending(p => p.Age);
91. What is the difference between First, FirstOrDefault and Single?
First throws when the sequence is empty, FirstOrDefault returns default(T) or null instead, and Single throws unless exactly one element matches. SingleOrDefault throws when more than one match exists. Choose based on whether "no match" and "many matches" are expected or exceptional.
92. Why is Any() preferred over Count() > 0?
Any stops as soon as the first element is found, while Count enumerates the whole sequence (or triggers a full count query). On large or remote sources this is a significant difference. The same idea applies to using Any(predicate) instead of Where(...).Any().
93. What does ToString() return by default and how do you customize it?
The base implementation returns the fully qualified type name, which is rarely useful in logs or UI. Override ToString to return a meaningful representation, and prefer it over concatenating properties manually. Records generate a helpful ToString automatically.
public override string ToString() => $"{Name} ({Age})";
94. What contract must Equals and GetHashCode satisfy?
If two objects are equal, they must return the same hash code, and equality must be reflexive, symmetric and transitive over an object's lifetime. GetHashCode must stay stable while the object sits in a hash-based collection. Overriding one without the other breaks Dictionary and HashSet lookups.
public override bool Equals(object? o)
=> o is Point p && p.X == X && p.Y == Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
95. What is a .csproj file and what does it typically contain?
The .csproj file is the MSBuild project definition: it lists the target framework, output type, package references and build settings. SDK-style projects are compact because the SDK supplies default item globs, so .cs files are included automatically. It is XML and should be edited with understanding, since mistakes break restore and build.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
96. What is the difference between TargetFramework and TargetFrameworks?
TargetFramework is singular and names one framework such as net8.0, while TargetFrameworks takes a semicolon-separated list to build the same code for several frameworks. Multi-targeting is used by libraries that must support both .NET Framework and modern .NET. The build then produces one output per framework.
97. What do dotnet new, dotnet build and dotnet run do?
dotnet new scaffolds a project from a template, dotnet build compiles it and writes binaries to bin, and dotnet run builds if needed and then executes the app. dotnet run is for local development, not deployment, because it depends on the SDK. All commands accept the project or solution path.
dotnet new console -n Demo
dotnet build Demo/Demo.csproj -c Release
dotnet run --project Demo
98. How do you add a NuGet package and what happens during restore?
dotnet add package writes a PackageReference with a version into the .csproj, and restore resolves the full dependency graph, downloading packages into the global cache. Restore also writes the assets file that the compiler uses to resolve references. Transitive dependencies are resolved automatically but can still cause version conflicts.
dotnet add package Serilog --version 3.1.1
dotnet restore
99. What is the difference between Debug and Release build configurations?
Debug emits full symbol information and disables most optimizations so breakpoints and stepping behave predictably. Release enables optimizations, may inline methods, and strips some debug metadata, producing faster and smaller output. Always validate performance and behavior with a Release build.
100. What is the difference between dotnet build and dotnet publish?
build compiles the project into bin for local execution, while publish produces a deployable folder including dependencies, runtime configuration and optionally the runtime itself. Publish supports self-contained and framework-dependent modes and can trim or single-file the output. Deploy the publish output, never raw build artifacts.
101. What is a .sln file used for?
A solution file groups multiple projects so tools can build and manage them together, recording project paths and build configurations. It is a container, not a build input for the compiler, and each project still builds from its own .csproj. The newer .slnx format is a lighter XML alternative.
102. What is Directory.Build.props used for?
It is an MSBuild file automatically imported by every project under its directory, so shared settings such as LangVersion, Nullable or TreatWarningsAsErrors are defined once. This keeps individual .csproj files small and consistent across a repository. Directory.Packages.props serves the same purpose for central package versions.
103. What do the ImplicitUsings and Nullable properties in the csproj change?
ImplicitUsings adds a set of common global using directives (System, System.Linq, System.Collections.Generic and others) so files need fewer usings. Nullable enable activates nullable reference type analysis and warnings. Both are on by default in new .NET templates.
104. How do you run unit tests from the command line?
dotnet test discovers and runs the test projects in the solution, restoring and building them first. Filters narrow the run to a class, category or name, and a trx logger produces a machine-readable report. Exit code is non-zero when tests fail, which is how CI detects problems.
dotnet test --filter "FullyQualifiedName~OrderTests"
105. How do breakpoints and stepping work in a .NET debugger?
A breakpoint pauses execution at a line so you can inspect locals, arguments and the call stack. Step Into (F11) enters the called method, Step Over (F10) executes it without entering, and Step Out (Shift+F11) finishes the current method. Continue (F5) resumes until the next breakpoint or exception.
106. When is a conditional breakpoint useful?
A conditional breakpoint pauses only when an expression is true, such as a specific id or an iteration count above a threshold. This avoids stopping thousands of times in a loop while hunting one bad case. Hit-count breakpoints are a variant that triggers after N hits.
// condition on a breakpoint
order.Id == 4711 && order.Total < 0
107. Where does Debug.WriteLine output go?
Debug.WriteLine writes to attached trace listeners, which the Visual Studio Output window shows during debugging. Console.WriteLine writes to standard output, which is visible in a console app but lost in a GUI or service. The calls are conditional on the DEBUG symbol and are removed from Release builds; Trace.WriteLine remains.
108. How do you read a call stack when debugging an exception?
The call stack lists frames from the current method outward, and the topmost frame is where execution stopped, which is where the exception was thrown for unhandled errors. Reading outward shows how that code was reached, which reveals the triggering input. Frames with optimized-away code may be missing in Release builds.
109. What are the Watch and Immediate windows used for?
The Watch window continuously evaluates expressions against the current debug context while you step. The Immediate window executes statements and method calls on the fly, which is handy for querying a collection or testing a fix. Both can change state, so use them carefully.
110. How do you debug async code that seems to hang?
First look for blocking calls such as .Result, .Wait() or Thread.Sleep in the call stack, since they cause the classic deadlock. The Tasks or Parallel Stacks window shows which tasks are awaiting what. Async methods also make stepping non-linear, so breakpoints after an await are often more useful than single-stepping.
111. What does enabling nullable reference types actually do?
It turns on compiler flow analysis for reference types: a non-nullable reference is expected never to be null, and a nullable one must be checked before use. Violations become warnings, not runtime errors, because the runtime is unchanged. The value is catching likely NullReferenceExceptions at compile time.
#nullable enable
string name = null; // warning CS8600
string? nickname = null; // fine
int len = nickname.Length; // warning CS8602
112. What does the null-forgiving operator ! do?
Appending ! to an expression suppresses nullable warnings by asserting that the value is not null. It changes no runtime behavior, so if the assertion is wrong you still get a NullReferenceException. Use it only where the compiler cannot see an invariant that you have verified.
string? name = Environment.GetEnvironmentVariable("USER");
Console.WriteLine(name!.Length); // compiler is silenced, not protected
113. What is the difference between is null and == null?
is null is a pattern that cannot be overloaded, so it always performs a true reference or nullable check. == null uses the type's overloaded equality operator, which may behave unexpectedly for types like Unity's Object or custom structs. Prefer is null and is not null for clarity and safety.
114. How do the null-conditional operators ?. and ?[] behave?
They short-circuit to null when the receiver is null instead of throwing, and the result type becomes nullable. This makes chained access safe without nested if blocks. Note that the whole chain yields null, so a following member access still needs care.
int? count = order?.Items?.Count;
string label = order?.Customer?.Name ?? "unknown";
115. How do you fix a CS8602 possible null reference warning properly?
Add a real check or guard clause before dereferencing so the compiler's flow analysis can see the invariant. Alternatives are the null-coalescing operator to supply a default, a pattern like "if (x is { } value)", or the [NotNull] style attributes on helper methods. Suppressing with ! should be the last resort.
if (customer is null)
throw new ArgumentNullException(nameof(customer));
Console.WriteLine(customer.Name); // no warning
116. What is a static class and when should you use one?
A static class cannot be instantiated or inherited and may contain only static members, which makes it a natural home for stateless helper methods or extension methods. It is convenient but hard to mock, so avoid it for logic that needs testing with dependencies. Static state is shared across the whole process and must be thread-safe.
117. What are object and collection initializers?
An object initializer sets properties or fields right after construction, and a collection initializer adds elements in the same expression. Both reduce boilerplate and work with any type exposing a parameterless constructor or an Add method. They do not replace a constructor for enforcing invariants.
var p = new Person { Name = "Ann", Age = 30 };
var tags = new List<string> { "a", "b" };
118. What is the default literal and what value does it produce?
default produces the default value of the inferred type: zero for numeric types, false for bool, and null for reference types and nullable value types. default(T) is the explicit generic form. A struct's default has all fields zeroed, which may be an invalid state for a struct with invariants.
int a = default; // 0
string? s = default; // null
DateTime d = default; // 01/01/0001
119. What is the difference between a shallow copy and a deep copy?
A shallow copy duplicates the outer object but shares referenced child objects, so changing a child is visible from both copies. A deep copy recursively clones the whole graph, giving fully independent instances. MemberwiseClone and record with expressions produce shallow copies.
120. What is the difference between a static member and an instance member?
An instance member belongs to each object and can access its state through this, while a static member belongs to the type and is shared by all instances. Static members cannot use this or instance fields. Static mutable state is a common source of concurrency bugs and test pollution.
121. What is the nameof operator good for?
nameof returns the compile-time name of a variable, member or type as a string, so refactoring renames update it automatically. It is commonly used in ArgumentNullException and logging where a string name is required. It has no runtime cost because the compiler substitutes a constant.
if (input is null)
throw new ArgumentNullException(nameof(input));
122. What is the difference between Environment.CurrentDirectory and AppContext.BaseDirectory?
Environment.CurrentDirectory is the process working directory, which can change at runtime and differs by how the app is launched. AppContext.BaseDirectory is the folder containing the application's assemblies and is stable. Use BaseDirectory to locate files shipped with the app and CurrentDirectory only when the user's context genuinely matters.
123. Why should you use Path.Combine instead of string concatenation for paths?
Path.Combine inserts the correct directory separator for the platform and handles trailing separators, avoiding malformed paths. Manual concatenation with "\\" breaks on non-Windows systems. Use Path.Join when you do not want later segments to override earlier absolute ones.
string full = Path.Combine(AppContext.BaseDirectory, "config", "app.json");
124. What is a GUID and when do you generate one?
A GUID is a 128-bit identifier that is practically unique without coordination, represented by the Guid struct. Guid.NewGuid creates a random (version 4) value, which is useful for entity keys, correlation ids and file names. It is not sequential, so it can fragment indexes compared with an integer or sequential GUID key.
Guid id = Guid.NewGuid();
Console.WriteLine(id.ToString("N")); // 32 hex chars
125. How do you measure how long a piece of code takes?
Stopwatch uses a high-resolution timer and is far more accurate than subtracting DateTime values. Start it, run the code, stop it, and read Elapsed or ElapsedMilliseconds. For real benchmarks use BenchmarkDotNet, since a single measurement is noisy.
var sw = Stopwatch.StartNew();
DoWork();
sw.Stop();
Console.WriteLine($"{sw.ElapsedMilliseconds} ms");
126. How do you validate a string format with a regular expression?
Regex.IsMatch returns true when the pattern matches anywhere in the input, so anchor with ^ and $ for whole-string validation. Compile or cache the Regex instance when it is reused in a hot path. Regex is powerful but hard to read, so keep patterns simple and documented.
var zip = new Regex(@"^\d{5}$");
bool ok = zip.IsMatch("12345");
127. How do you add days or hours to a DateTime?
DateTime is immutable, so AddDays, AddHours and similar methods return a new value instead of modifying the original. Combine them with a TimeSpan for durations, and use DateTimeOffset when offsets matter. Forgetting to use the returned value is a classic bug.
var now = DateTime.UtcNow;
var due = now.AddDays(7).AddHours(-2);
var span = due - now; // TimeSpan
128. How do you join a collection of strings into one string?
string.Join concatenates elements with a separator and handles null elements as empty strings, which is safer than manual concatenation with separators. It accepts any IEnumerable and an optional start index and count. For object collections, project to strings first with Select.
string csv = string.Join(", ", names);
string line = string.Join(';', items.Select(i => i.Id));
129. What is the difference between IEnumerable<T> and List<T>?
IEnumerable<T> is a read-only, forward-only sequence contract that supports lazy evaluation and can represent a query, a file or a database result. List<T> is a concrete mutable collection with indexing, Add and Remove. Accept IEnumerable in method parameters for flexibility and return List or IReadOnlyList when callers need materialized data.
130. What is the difference between IEnumerable and IQueryable?
IQueryable carries an expression tree so a provider such as EF Core can translate the query into SQL and execute it remotely. IEnumerable executes delegates in memory, which means filtering happens after all rows are fetched. Mixing them by calling AsEnumerable too early is a common performance mistake.
131. What is a lambda expression and how do Func and Action relate to it?
A lambda is an anonymous function written as parameters => body, which the compiler converts into a delegate or expression tree. Func<T> represents a method with a return value and Action represents one without. Lambdas capture variables from the enclosing scope, which the compiler lifts into a closure.
Func<int, int> square = x => x * x;
Action<string> log = msg => Console.WriteLine(msg);
Console.WriteLine(square(4));
132. What is a delegate in C#?
A delegate is a type-safe reference to a method with a specific signature, so behavior can be passed as a value. Custom delegates are declared with the delegate keyword, though Func and Action cover most cases now. Multicast delegates hold several targets and invoke them in order.
public delegate int Operation(int a, int b);
Operation add = (a, b) => a + b;
int result = add(2, 3);
133. What is the difference between Array.Sort and LINQ OrderBy?
Array.Sort sorts in place and mutates the array, while OrderBy returns a new lazy sequence and leaves the source unchanged. OrderBy is stable, so equal keys keep their original order, whereas Array.Sort is not guaranteed stable. Sorting in place is faster and allocation-free when mutation is acceptable.
134. How do you remove duplicates from a collection?
Passing the sequence to a HashSet removes duplicates by equality, and Distinct does the same lazily while preserving first occurrence order. For custom types, equality depends on Equals and GetHashCode or on a custom IEqualityComparer. Choose based on whether you need the result as a set or as an ordered list.
var unique = items.Distinct().ToList();
var set = new HashSet<string>(names);
135. Why do int and Int32 mean the same thing?
Keywords such as int, string and bool are aliases for the BCL types System.Int32, System.String and System.Boolean, so they compile to identical code. The lowercase keyword is preferred by convention in C# code. Using the alias or the full name has no runtime cost.
136. What does a #nullable directive control?
#nullable enable, disable, restore and annotations change the nullable analysis context inside a file, which is useful when a project enables nullable globally but a legacy file is not ready. restore returns to the project setting, so it pairs with enable. The directive affects compiler warnings only.
137. What is an exception filter with when?
An exception filter adds a condition to a catch clause, so the handler runs only when the predicate is true; otherwise the search for a handler continues without unwinding the stack. This is useful for inspecting an error code or logging selectively. Filters cannot contain code with side effects that must run.
try { Call(); }
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
138. How do you define a custom exception type?
Derive from Exception, provide the three standard constructors (parameterless, message, message plus inner exception), and mark the type serializable if needed. Name it with an Exception suffix and keep it focused on one failure condition. Throw it only for genuinely exceptional situations.
public class OrderNotFoundException : Exception
{
public OrderNotFoundException(int id)
: base($"Order {id} was not found.") { }
}
139. What is the multiple enumeration problem with IEnumerable?
Because a sequence may be lazy, enumerating it twice can execute the underlying query or I/O twice, wasting work or producing different results. This often appears when you call Count() and then foreach over the same query. Materialize once with ToList when you need several passes.
var list = query.ToList(); // one execution
int n = list.Count;
foreach (var x in list) { }
140. How do you convert a string to an enum safely?
Enum.TryParse returns false instead of throwing when the text is not a valid name or number, and ignoreCase controls matching. A successful parse of a number can still produce an undefined value, so validate with Enum.IsDefined when that matters. Enum.Parse is the throwing alternative.
if (Enum.TryParse<Status>("Active", true, out var status)
&& Enum.IsDefined(status))
{
Console.WriteLine(status);
}
141. What is encapsulation and how is it implemented in C#?
Encapsulation means hiding internal state and exposing only a controlled public surface, so callers cannot put an object into an invalid state. In C# it is implemented with access modifiers, properties that validate values, and private fields. The goal is not secrecy but protecting invariants and allowing the implementation to change without breaking consumers.
public class Account
{
private decimal _balance;
public decimal Balance => _balance;
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
_balance += amount;
}
}
142. What is the difference between a field and a property?
A field is a storage location; a property is a pair of accessors (get/set) that behave like a field to callers but can contain logic. Properties support validation, lazy initialization, change notification, and different access levels per accessor. Public fields cannot do any of that and also break binary compatibility when you later convert them into properties, so the convention is private fields plus public properties.
143. Why should public fields generally be avoided?
Public fields expose raw storage, so any caller can bypass validation and invariants, and you cannot add logging or notifications later without breaking callers. Changing a field to a property is a binary-breaking change for compiled assemblies, while changing a property's body is not. Interfaces also cannot declare fields, which limits abstraction.
144. What access modifiers exist in C# and what does each allow?
public is visible everywhere, private only inside the containing type, protected to derived types, and internal within the same assembly. protected internal is the union of protected and internal, while private protected is the intersection (derived types in the same assembly). Top-level types can only be public or internal; members default to private.
145. What is inheritance and what are its limitations in C#?
Inheritance lets a class reuse and specialize a base class, establishing an "is-a" relationship. C# supports single class inheritance only, although a class can implement many interfaces. Inheritance creates tight coupling, so composition is often preferred when the relationship is really "has-a" or when behavior varies independently.
146. What is polymorphism in C# and what forms does it take?
Polymorphism is the ability to treat objects of different types through a common abstraction. C# has compile-time polymorphism (overloading, resolved by the compiler from static types) and runtime polymorphism (virtual/abstract members dispatched through the object's actual type). Interface dispatch is the usual way to get runtime polymorphism without a shared base class.
abstract class Shape { public abstract double Area(); }
class Circle : Shape
{
public double R { get; init; }
public override double Area() => Math.PI * R * R;
}
Shape s = new Circle { R = 2 };
Console.WriteLine(s.Area()); // resolved at runtime
147. What is the difference between overloading and overriding?
Overloading means several members with the same name but different signatures in the same type; the compiler picks one from the static argument types. Overriding replaces a virtual or abstract base implementation at runtime, and the signature must match exactly. Overloading is resolved at compile time, overriding at run time.
148. What is an abstract class?
An abstract class cannot be instantiated and may declare abstract members that derived classes must implement, while also providing shared state and concrete behavior. It is appropriate when related types share an implementation and a partial contract. A derived class must implement every abstract member unless it is itself abstract.
149. What is an interface in C#?
An interface declares a contract of members that implementing types must provide, without specifying state. Since C# 8 interfaces may also contain default implementations, static members, and private helpers, but they still cannot hold instance fields. Interfaces enable multiple inheritance of behavior contracts and loose coupling.
150. When should you prefer an abstract class over an interface?
Choose an abstract class when implementations share real code or state and the types form a genuine "is-a" hierarchy with a single base. Choose an interface when unrelated types must satisfy a capability and you want multiple implementation or versioning through default members. Many designs use both: an interface for the contract plus an abstract base class supplying a default implementation.
151. Can a class inherit from one base class and implement several interfaces?
Yes. C# allows exactly one base class but any number of interfaces, and the base class must be listed first in the declaration. If two interfaces declare the same member signature, a single public implementation satisfies both, while explicit implementation can keep them separate. Structs can implement interfaces but cannot inherit from a class.
152. What is the difference between virtual, override, and new?
virtual declares a member that derived classes may replace and that is dispatched through the runtime type. override replaces an inherited virtual member and extends the virtual slot. new hides an inherited member by name instead of overriding it, and which member is called then depends on the static type of the reference.
153. What does the sealed keyword do?
On a class, sealed prevents further inheritance; on an override, it stops further overriding of that member in derived types. Sealing can help the JIT devirtualize calls and it documents design intent, but it closes extensibility. A sealed class can still implement interfaces and be composed.
154. Can you override a member that was marked sealed override?
No. A sealed override occupies the virtual slot permanently, so any derived class that declares the same member must use new, which hides instead of overriding. That is a common way to lock down behavior inherited from a framework base type.
155. What is method hiding and why can it be dangerous?
Hiding declares a member with the same name in a derived class without override, so the base member still exists and is called whenever the reference type is the base class. The result depends on the static type of the variable, which surprises readers and breaks polymorphism. The compiler only warns (CS0108), so accidental hiding can ship silently.
class A { public void Run() => Console.WriteLine("A"); }
class B : A { public new void Run() => Console.WriteLine("B"); }
A x = new B();
x.Run(); // "A" - hiding, not polymorphism
((B)x).Run(); // "B"
156. What is the purpose of the base keyword?
base gives access to the inherited implementation or constructor, which is essential when an override wants to extend rather than replace behavior. In a constructor it forwards arguments to a specific base constructor. It always refers to the immediate base class, not the root type.
157. How does abstraction differ from encapsulation?
Abstraction reduces complexity by exposing a simplified model of a concept and hiding irrelevant detail; encapsulation is the mechanism that enforces the boundary. You design an abstraction (an interface or abstract class) and use encapsulation (access modifiers, properties) to protect it. In practice abstraction is a design decision, encapsulation is an implementation technique.
158. What is the difference between Object.Equals and the == operator?
== is a static, overloadable operator resolved at compile time, while Equals is a virtual method dispatched at runtime. For reference types without an overload, == compares references; for string and some value types it compares values. Collections and generic code call Equals, so overriding one without the other produces inconsistent behavior.
159. Why must GetHashCode be overridden together with Equals?
Hash-based collections require that equal objects produce the same hash code, otherwise a lookup can miss an object that is present. If two objects are equal by Equals but have different hashes, HashSet and Dictionary treat them as different keys. The hash must be built from the same members used for equality and must not change while the object is in a hash collection.
public override bool Equals(object? obj) =>
obj is Order o && o.Id == Id;
public override int GetHashCode() => HashCode.Combine(Id);
160. What is the difference between reference equality and value equality?
Reference equality asks whether two variables point to the same object instance, which is ReferenceEquals or the default == for classes. Value equality asks whether two objects represent the same data, which requires an Equals override, a record, or a struct with implemented comparison. Structs get field-by-field value equality by default; classes do not.
161. What does ToString do and why override it?
ToString returns a string representation and is called implicitly by string interpolation, concatenation, and debuggers. The default returns the type's full name, which is rarely useful in logs. Override it for diagnostic output, but keep it cheap, side-effect free, and culture-aware when the text is user-facing.
162. What is IComparable used for?
IComparable<T> defines the natural ordering of a type through CompareTo, returning a negative number, zero, or a positive number. Implementing it enables List.Sort(), Array.Sort, and default comparers in sorted collections. It should be consistent with Equals to avoid surprising results.
public int CompareTo(Money? other) =>
other is null ? 1 : Amount.CompareTo(other.Amount);
163. What is IComparer and when do you use it instead of IComparable?
IComparer<T> is an external comparison strategy passed to sort methods, so it lets you order a type in several ways without changing the type itself. Use it when you do not own the type, when ordering depends on the context, or when you need multiple orderings. Comparer<T>.Create gives a quick way to build one from a lambda.
var byName = Comparer<Person>.Create((a, b) =>
string.Compare(a.Name, b.Name, StringComparison.Ordinal));
people.Sort(byName);
164. What is the difference between IEquatable and overriding Equals(object)?
IEquatable<T> provides a strongly typed Equals(T) that avoids boxing and casting in generic collections, which matters for structs. Overriding Equals(object) is still required for non-generic callers, so a well-behaved value type usually does both. Generic collections check for IEquatable<T> first, so implementing it is a real performance win.
165. What are generics and what problem do they solve?
Generics let you define types and methods parameterized by type, giving compile-time type safety and avoiding boxing, casts, and duplicated code. Before generics, reusable containers were built on object, which pushed type errors to runtime. Generic instantiations over reference types share one JIT-compiled implementation, so there is little code bloat.
166. What generic constraints are available in C#?
The main constraints are class, struct, new(), notnull, unmanaged, a base class, an interface, and another type parameter (where T : U). Constraints are checked at compile time for callers and enable members of the constrained type inside the generic body. Multiple constraints are combined in one where clause, with the class type first and new() last.
167. What does the new() constraint require?
new() requires the type argument to have a public parameterless constructor, which allows the generic code to call new T(). It cannot be combined with struct (structs already satisfy it) or with an abstract class constraint. It also implies the type is not nullable in the reference-type sense.
168. What is the difference between where T : class and where T : struct?
class constrains to reference types and allows null; struct constrains to non-nullable value types and gives access to default(T) without null checks. With struct the compiler knows T is a value type and never boxes in comparisons against default. Note that Nullable<T> does not satisfy struct.
169. What is variance in generic interfaces?
Covariance (out T) lets you use a more derived type argument where a less derived one is expected, as with IEnumerable<string> to IEnumerable<object>. Contravariance (in T) goes the other way and is used for consumers such as IComparer<object> to IComparer<string>. Variance requires reference types and is only allowed on interfaces and delegates where the parameter position permits it.
170. How does generic type inference work?
When you call a generic method without explicit type arguments, the compiler infers them from the argument types and their implicit conversions. If inference is ambiguous or fails, you must specify the arguments explicitly. Inference does not consider the return type, which is why some APIs need an explicit type argument or a helper class.
171. What is the difference between a generic method and a generic type?
A generic type parameterizes the whole type, so every member can use it, while a generic method parameterizes only that method and can appear in a non-generic type. Generic methods make static helper APIs easier to call because inference works per call site. Both compile to a single definition with type parameters substituted at runtime.
172. How do you get the default value of a generic type parameter?
Use default(T) or the target-typed default literal, which yields null for reference types and zeroed bits for value types. With C# 9 and nullable reference types you may need T? or default! to express intent. Comparing a T against default with == requires an unconstrained-safe pattern such as EqualityComparer<T>.Default.
173. Why can't you reliably use == with an unconstrained generic type parameter?
The compiler does not know whether T is a reference type with an overloaded ==, a value type, or a type without that operator, so a == b does not compile for unconstrained T. Use EqualityComparer<T>.Default.Equals(a, b) instead, which respects IEquatable<T>, overrides, and null semantics. That also avoids boxing for value types.
174. What is a static constructor and when does it run?
A static constructor runs once per closed type, before the first static member access or first instance creation, and the runtime guarantees it is not run concurrently twice. It cannot take parameters or access modifiers and exceptions from it surface as TypeInitializationException, permanently breaking the type. Prefer static field initializers or lazy initialization when failure must be recoverable.
175. What is the difference between static and instance members?
Static members belong to the type and are shared by all instances; instance members belong to a specific object and can access its state. Static state is effectively global within the application domain, so it must be thread-safe and is harder to test. Static methods cannot use this or be virtual.
176. Can a static class have a constructor, and can it be instantiated?
A static class cannot be instantiated or inherited and may contain only static members; it can have a static constructor to initialize static fields. The compiler seals and abstracts it automatically. Extension methods require a static, non-nested, non-generic class.
177. What are nested types and when are they useful?
A nested type is declared inside another type and can access the enclosing type's private members, which makes it useful for implementation details such as builders, enumerators, or caches. Nesting also expresses a strong ownership relationship and keeps the namespace clean. Nested types do not automatically get an instance of the outer type, unlike in Java.
178. What is a partial class used for?
partial splits one type across several files that the compiler merges, which is how source generators and designers add code without touching your file. Attributes and interfaces are combined across parts, but each part must be marked partial and be in the same assembly. It is purely a compile-time feature with no runtime cost.
179. What are partial methods?
A partial method declares a signature in one part of a partial type and optionally implements it in another; if no implementation exists, calls are removed by the compiler. Classic partial methods must return void and cannot have out parameters, though C# 9 lifted most of these restrictions when an implementation is required. They are typically used by code generators to expose optional hooks.
180. What is an indexer?
An indexer lets an object be indexed with [ ] syntax by declaring this[parameter] with get and/or set accessors. It can take any parameter types and be overloaded, which makes wrapper types feel like collections. Indexers are members, not properties, and can be virtual or implemented explicitly from an interface.
public class Grid
{
private readonly int[,] _cells = new int[10, 10];
public int this[int row, int col]
{
get => _cells[row, col];
set => _cells[row, col] = value;
}
}
181. Can a type declare more than one indexer?
Yes, indexers can be overloaded as long as the parameter lists differ, for example by index type or arity. They can also differ in return type only if the parameter list differs, because the signature is based on parameters. A common pattern is an int indexer plus a string key indexer.
182. What is explicit interface implementation and when do you need it?
Explicit implementation declares a member as void IFoo.Bar(), so it is reachable only through the interface reference and is hidden from the class's public surface. You need it when two interfaces declare conflicting members with the same signature, or when you want a member used only via the abstraction. It cannot be virtual, abstract, or accessed through the class type.
class Logger : IDisposable
{
void IDisposable.Dispose() => Flush();
public void Flush() { /* ... */ }
}
183. What happens when a class implements two interfaces with the same member signature?
One public member satisfies both interfaces if the signature and return type match, which is usually what you want. If the behaviors must differ, implement at least one explicitly so callers get the right one through each interface. This is exactly how generic and non-generic GetEnumerator coexist on collection types.
184. What is operator overloading and when should you avoid it?
Operator overloading defines what +, ==, !=, implicit conversions, and similar operators mean for your type, which is appropriate for value-like types such as Money or Vector. Avoid it when the meaning is not obvious or when it hides expensive work, and never make it surprising or lossy. Overloaded == and != must be defined as a pair.
public readonly record struct Money(decimal Amount)
{
public static Money operator +(Money a, Money b) =>
new(a.Amount + b.Amount);
}
185. Which operators must be overloaded together?
== and != must both be defined, and if either is overloaded the compiler expects Equals and GetHashCode overrides too for consistency. Comparison operators < and > must be paired, as must <= and >=. Implicit and explicit conversion operators are independent but should not bypass validation.
186. What are object initializers and collection initializers?
Object initializers assign properties after construction with new T { A = 1, B = 2 }, while collection initializers add elements with new List<int> { 1, 2, 3 } by calling Add. Both are purely syntactic sugar over constructor plus assignments, so they require an accessible parameterless constructor unless you also pass arguments. Collection initializers require the type to implement IEnumerable and have an applicable Add method.
187. What is the difference between an object initializer and a constructor?
A constructor runs inside the type and can enforce invariants across all arguments; an object initializer runs after construction and sets members one by one, so intermediate invalid states are possible. Initializers also cannot be intercepted by the type, which matters for immutability. For immutable types prefer constructors or init accessors.
188. What is the difference between an array and a List<T>?
An array has a fixed length and is allocated as one contiguous block, so it has the lowest overhead but cannot grow. List<T> wraps an array, grows it geometrically by doubling, and exposes Add, Remove, and other conveniences at the cost of extra indirection and unused capacity. Use arrays for fixed-size, hot data and lists for general-purpose mutable sequences.
189. When would you use a Dictionary instead of a List?
Use a Dictionary<TKey, TValue> when you need lookup by key in near-constant time; a list lookup is a linear scan and becomes slow as it grows. A dictionary costs more memory and loses insertion order, so a list is better for small collections, ordered iteration, or index-based access. If you need both, keep a list and a dictionary in sync.
190. What is a HashSet and when is it preferable to a List?
HashSet<T> stores unique elements with O(1) average Contains, Add, and Remove, making it ideal for membership tests and de-duplication. A list preserves order and allows duplicates and indexing, which a set does not. HashSet also offers set operations such as UnionWith and IntersectWith.
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var tag in tags)
if (seen.Add(tag)) Console.WriteLine(tag); // unique only
191. What is the difference between Queue and Stack?
Queue<T> is first-in-first-out with Enqueue/Dequeue, while Stack<T> is last-in-first-out with Push/Pop. Both are O(1) amortized and both offer Peek and TryDequeue/TryPop. Use a queue for breadth-first or work distribution, a stack for depth-first or undo.
192. What are the main trade-offs of Dictionary<TKey, TValue>?
It offers O(1) average lookup but relies on good hash distribution, so a poor GetHashCode degrades it to linear probing and can cause severe slowdowns. It is not thread-safe for concurrent writes and does not preserve insertion order. It also throws on duplicate keys with Add but overwrites with the indexer, so choose deliberately.
193. What happens if you mutate an object used as a dictionary key?
The entry is stored in the bucket computed from the original hash, so after mutation the lookup searches a different bucket and reports the key as missing. The item is effectively lost and cannot be removed reliably until rehashed. Keys should therefore be immutable, or at least not participate in GetHashCode after insertion.
194. What is IEnumerable<T> and what does it guarantee?
IEnumerable<T> exposes a forward-only cursor over a sequence through GetEnumerator, and that is all it guarantees: you can iterate, possibly many times, possibly with different results. It says nothing about count, indexing, or mutability. It is the minimal contract that LINQ and foreach build on.
195. What is the difference between IEnumerable, ICollection, and IList?
IEnumerable<T> only supports iteration, ICollection<T> adds Count, Add, Remove, and Contains, and IList<T> adds index access and Insert. Each step trades generality for capability, so accept the weakest interface your method actually needs. Returning IReadOnlyList<T> is a good compromise for read-only indexed data.
196. What is the difference between IEnumerable and IQueryable?
IEnumerable<T> executes delegates in memory with LINQ-to-Objects, while IQueryable<T> carries an expression tree that a provider translates, for example into SQL. With IQueryable the filtering happens on the server only if you keep composing before materialization. Calling AsEnumerable too early silently pulls the whole table into memory.
197. What is deferred execution in LINQ?
Most LINQ operators only build a pipeline; nothing is enumerated until you iterate or call a materializing operator such as ToList, Count, or First. This allows composition and lets providers translate the whole query, but it also means the source can change before execution. Operators that must return a single value or a collection execute immediately.
var query = numbers.Where(n => n > 2); // nothing runs yet
numbers.Add(99);
foreach (var n in query) Console.WriteLine(n); // 99 is included
198. What is the problem with enumerating a sequence more than once?
A deferred query re-executes on every enumeration, so repeated iteration can cause duplicate database round-trips, repeated file reads, or non-deterministic results if the source changed. It is easy to do accidentally, for example calling Count() and then iterating. Materialize once with ToList when you need multiple passes.
199. How do you detect or prevent accidental multiple enumeration?
Accept and return IReadOnlyList<T> or ICollection<T> when the data will be used more than once, and materialize at the boundary where the query is defined. Analyzers such as the Roslyn multiple-enumeration rules catch the pattern statically. Inside a method, call ToList() once and use the list everywhere.
200. How does GroupBy work in LINQ?
GroupBy builds a lookup keyed by the selector and buffers the whole sequence, because it must read everything before yielding the first group. Each result is an IGrouping<TKey, TElement> that is itself enumerable. Use the element selector and result selector overloads to avoid projecting twice.
var byCity = people
.GroupBy(p => p.City, p => p.Name)
.Select(g => new { City = g.Key, Count = g.Count() });
201. What is the difference between Select and SelectMany?
Select maps each element one-to-one, producing a sequence of the same length, while SelectMany maps each element to a sequence and flattens the results into one. SelectMany is the operator for nested collections and for cross joins. Forgetting to flatten leaves you with IEnumerable<IEnumerable<T>>.
202. How does Join work and how does it differ from GroupJoin?
Join performs an inner equi-join, emitting one result per matching pair and dropping unmatched elements from both sides. GroupJoin emits one result per outer element with all its matches, which you can flatten to emulate a left outer join. Both use a hash lookup on the inner sequence, so the key selectors should be simple and consistent.
var q = from o in orders
join c in customers on o.CustomerId equals c.Id
select new { c.Name, o.Total };
203. What does Aggregate do and when is it appropriate?
Aggregate folds a sequence into a single value by repeatedly applying a function with an accumulator, optionally starting from a seed. It is appropriate for custom folds that no built-in operator covers, such as running products or building a string. For simple sums, counts, or concatenations the dedicated operators are clearer and often faster.
var product = numbers.Aggregate(1, (acc, n) => acc * n);
204. Why is Any() better than Count() > 0?
Any() stops after the first element, while Count() > 0 may enumerate the entire sequence. Against a database, Count() becomes a SELECT COUNT(*) over the whole set, which is far more expensive than an existence check. Any(predicate) also short-circuits, unlike Count(predicate) == 0.
205. What is the difference between All and Any?
Any() asks whether at least one element matches, while All() asks whether every element matches. Both short-circuit: Any stops at the first match, All at the first mismatch. Note that All on an empty sequence returns true, which is mathematically correct but can surprise callers.
206. When should you use First, FirstOrDefault, or Single?
First takes the first match and throws if none exists, FirstOrDefault returns the default instead, and Single throws unless exactly one match exists. Use Single to assert uniqueness, such as lookup by primary key. Prefer the *OrDefault variants only when absence is a normal, expected case.
207. What is the difference between Where(...).Count() and Count(predicate)?
The overload Count(predicate) does the filtering and counting in one pass with no intermediate iterator allocation. Where(...).Count() composes two operators, adding a small amount of overhead and an extra allocation per call. The result is identical, so prefer the predicate overload in hot paths.
208. What is a lambda expression?
A lambda is an anonymous function written inline that can be converted to a delegate or an expression tree. Its parameter types are usually inferred, and it can capture variables from the enclosing scope. Statement-bodied lambdas use braces, expression-bodied ones do not.
209. What is a closure and what are its pitfalls?
A closure is created when a lambda captures a local variable or parameter; the compiler lifts that variable into a compiler-generated class so it outlives the stack frame. Captured variables are shared by reference, so all delegates see the latest value, and the captured state keeps objects alive longer than expected. In hot paths, closures also allocate, which matters for performance-sensitive code.
Func<int> MakeCounter()
{
int count = 0;
return () => ++count; // count is lifted to a closure
}
210. Why did capturing a loop variable used to be a bug, and what changed?
Before C# 5, foreach declared a single variable reused across iterations, so lambdas created in the loop all captured the same variable and observed its final value. Since C# 5, foreach creates a fresh variable per iteration, so the pattern works naturally. A plain for loop still declares one variable, so you must copy it into a local inside the body.
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy)); // 0,1,2
}
211. What is the difference between Func, Action, and Predicate?
Func<...> returns a value, Action<...> returns void, and Predicate<T> is a specialized delegate returning bool for a single argument. Predicate<T> predates Func<T, bool> and appears mostly in older APIs such as List.Find. They are not interchangeable without an adapter because they are distinct delegate types.
212. What is the difference between Func<T> and Expression<Func<T>>?
Func<T> is compiled code you can invoke directly, while Expression<Func<T>> is a data structure describing the code, which a provider can inspect and translate. LINQ-to-Entities uses expression trees so it can generate SQL instead of running the lambda in memory. You can compile an expression with .Compile(), but that loses translatability and costs startup time.
213. What is a delegate?
A delegate is a type-safe reference to a method with a specific signature; an instance holds a target object and a method pointer, so it can represent instance, static, or lambda code. Delegates are immutable, so += creates a new combined delegate rather than modifying the original. They are the foundation of callbacks, events, and LINQ.
214. How do multicast delegates behave with return values?
A multicast delegate holds an invocation list and calls every target in order, but only the return value of the last one is observed, so return values are effectively lost. Exceptions stop the chain, so later subscribers never run. This is why events are normally typed as void-returning delegates.
215. What is an event and how does it differ from a public delegate field?
An event exposes only += and -= to outside code, so subscribers cannot invoke it, replace the whole invocation list, or clear other handlers; a public delegate field allows all of that. Events therefore protect the publisher's contract and are the correct choice for notifications. Inside the declaring class the event behaves like a delegate field.
public event EventHandler<EventArgs>? Changed;
protected virtual void OnChanged() =>
Changed?.Invoke(this, EventArgs.Empty);
216. Why check an event for null before raising it?
An event with no subscribers is null, so invoking it throws NullReferenceException. Copying it into a local first avoids a race where a subscriber unsubscribes between the null check and the invocation. The null-conditional call Changed?.Invoke(...) is the concise modern form.
217. Can events cause memory leaks?
Yes. The publisher holds a reference to every subscriber through the delegate, so a long-lived publisher keeps short-lived subscribers alive until they unsubscribe. This is a classic leak in static events, long-lived services, and UI handlers. Always unsubscribe in Dispose or use weak event patterns for long-lived publishers.
218. What is an anonymous type?
An anonymous type is an immutable compiler-generated class created with new { A = 1, B = "x" }, useful mainly for projections in LINQ queries. It gets value equality, a readable ToString, and read-only properties. It cannot be returned from a method typed as object and later used statically, because its name is not accessible.
219. Why can't an anonymous type cross a method boundary?
Its type has no accessible name outside the assembly, so you cannot declare a variable, parameter, or return type for it. You can pass it as object or use dynamic, but both lose compile-time safety. For public APIs use a named type, a tuple, or a record instead.
220. What are nullable reference types?
Nullable reference types are a compile-time annotation system: with <Nullable>enable</Nullable>, string means "not expected to be null" and string? means "may be null", and the compiler warns about possible dereferences. The annotations are erased at runtime and the CLR still allows null, so they are contracts and tooling rather than enforcement. They also make intent explicit in public signatures.
221. What does the null-forgiving operator do?
The postfix ! suppresses the nullable warning for an expression without changing runtime behavior. It is appropriate when you have knowledge the compiler lacks, such as a field initialized by a framework or a validated parameter. Using it routinely hides real bugs, so prefer restructuring the code or adding a proper guard.
string? input = Console.ReadLine();
string value = input!; // compiler trusts you; still null at runtime
222. What is the difference between ?. and ?? and ??=?
?. short-circuits member access when the receiver is null and yields null, ?? supplies a fallback for a null left operand, and ??= assigns only when the target is null. They compose well, for example cache[key] ??= Build(). ?. on a value type returns T?, so unwrap carefully.
223. How does garbage collection work in .NET?
The GC is a tracing, generational collector that finds live objects starting from roots such as stack locals, statics, and handles, then reclaims everything unreachable and compacts the heap. It runs on a background thread for most generations and can pause application threads briefly. Because it moves objects, references are updated automatically, which is why you cannot pin objects without cost.
224. What are the GC generations and why do they exist?
Gen 0 holds new short-lived objects, Gen 1 is a buffer for objects that survived one collection, and Gen 2 holds long-lived objects. The generational hypothesis is that most objects die young, so collecting Gen 0 frequently is cheap and efficient. Surviving objects are promoted, which is why keeping large graphs alive briefly is expensive.
225. What is the Large Object Heap?
Objects of 85,000 bytes or more are allocated on the Large Object Heap, which is collected only with Gen 2 and is not compacted by default. That leads to fragmentation and pause times when large buffers are allocated repeatedly. Reuse buffers with ArrayPool<T> instead of allocating big arrays per request.
226. What typically triggers a Gen 2 collection?
A Gen 2 collection happens when the Gen 0 or Gen 1 budget is exceeded after promotion, when memory pressure is high, or when GC.Collect is called explicitly. Since it must trace the whole heap, it is the most expensive collection, so GC.Collect in production code usually hurts throughput. Let the runtime tune itself unless you have measured evidence.
227. What is the difference between managed and unmanaged resources?
Managed resources are .NET objects the GC reclaims automatically; unmanaged resources are things like file handles, sockets, database connections, and native memory that the GC does not understand. Unmanaged resources must be released deterministically, which is what IDisposable and finalizers exist for. A managed wrapper can still hold an unmanaged handle, so disposing the wrapper matters.
228. Why do finalizers make garbage collection more expensive?
An object with a finalizer cannot be reclaimed in the collection that finds it unreachable; it is queued for the finalizer thread and promoted to an older generation. That means it survives at least one extra collection and often reaches Gen 2 before being collected, and the finalizer thread can become a bottleneck. This is the main reason to avoid finalizers and prefer IDisposable.
229. What is IDisposable for?
IDisposable provides deterministic cleanup of resources through Dispose, which the using statement and declaration call automatically, even when an exception occurs. It complements the GC, which only reclaims memory and at an unpredictable time. Anything holding a handle, connection, or unmanaged buffer should implement it.
using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
Console.WriteLine(reader.ReadLine());
230. What does the full dispose pattern look like?
The pattern has a public Dispose() that disposes managed and unmanaged resources, calls GC.SuppressFinalize(this), and a protected Dispose(bool disposing) that only touches managed objects when disposing is true. A finalizer calls Dispose(false) to release unmanaged handles only. Derived classes override Dispose(bool) and call base.Dispose(disposing).
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) _managed?.Dispose();
_handle.Close(); // unmanaged
_disposed = true;
}
231. When do you actually need a finalizer?
Only when the type owns unmanaged memory or a native handle that is not already wrapped by a safe handle, and you cannot rely on callers to dispose. If you wrap a SafeHandle, the finalizer is unnecessary because the safe handle finalizes itself. Finalizers should never block, allocate heavily, or touch other managed objects.
232. What is the difference between the stack and the heap?
The stack is a per-thread, LIFO region for method frames and locals; allocation is just a pointer move, and memory is reclaimed when the method returns. The heap is a shared region managed by the GC for objects whose lifetime is not tied to a single frame. Stack access is faster, but its size is limited and contents cannot outlive the call.
233. Are structs always allocated on the stack?
No. A struct is allocated inline wherever it is stored: on the stack as a local, inside its containing object on the heap as a field, or inside an array's contiguous buffer. Boxing a struct moves a copy to the heap. The correct mental model is "value types are stored inline, reference types are stored by reference".
234. What are boxing and unboxing?
Boxing wraps a value type in a heap object so it can be used as object or an interface; unboxing extracts the value with a type check and copy. Both cost an allocation or a cast, and unboxing to the wrong type throws InvalidCastException. Generics and IEquatable<T> exist largely to avoid this.
int n = 42;
object boxed = n; // boxing: new heap object
int back = (int)boxed; // unboxing: copy
235. What is reflection and when is it appropriate?
Reflection lets code inspect and invoke types, members, and attributes at runtime through System.Reflection. It is appropriate for frameworks, serializers, dependency injection containers, and plugin loading where types are unknown at compile time. In ordinary application code it usually signals a design that could be expressed with an interface instead.
236. Why is reflection considered slow, and how do you mitigate it?
Reflection bypasses static binding, allocates metadata objects, and often prevents inlining and devirtualization, so a naive per-call lookup is orders of magnitude slower than a direct call. The standard mitigation is to cache MethodInfo or PropertyInfo objects, or to compile delegates once with Expression.Lambda(...).Compile(). Source generators and compiled expressions are the fastest options for hot paths.
private static readonly Func<Order, int> GetId =
(Func<Order, int>)Delegate.CreateDelegate(
typeof(Func<Order, int>),
typeof(Order).GetProperty(nameof(Order.Id))!.GetGetMethod()!);
237. What is an attribute?
An attribute is declarative metadata attached to an assembly, type, or member and stored in the assembly's metadata table. It does not change behavior by itself; some consumer, often reflection or the compiler, must read and act on it. Examples include [Obsolete], [Serializable], and [JsonPropertyName].
238. How do you read attributes at runtime?
Call GetCustomAttribute<T> or GetCustomAttributes<T> on a Type, MemberInfo, or assembly, optionally with inherit: true to walk the base chain. Attribute instances are created on demand, so cache them if you read them frequently. Attribute.IsDefined is a cheaper existence check.
var marker = method.GetCustomAttribute<ObsoleteAttribute>();
if (marker is not null)
Console.WriteLine($"{method.Name} is obsolete: {marker.Message}");
239. How do you define a custom attribute?
Derive from Attribute, by convention name the class with an Attribute suffix, and expose the data through constructor parameters or settable properties with simple types. Keep it passive and immutable; the logic belongs to the code that reads it. Mark it [AttributeUsage] to control where it may be applied.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class CachePolicyAttribute : Attribute
{
public CachePolicyAttribute(int seconds) => Seconds = seconds;
public int Seconds { get; }
}
240. What does AttributeUsage control?
AttributeTargets restricts where the attribute may be applied, AllowMultiple permits more than one instance on the same target, and Inherited controls whether derived types or overrides see it when queried with inherit: true. Getting these wrong produces either compile errors for valid usage or duplicate data at runtime. The defaults are all targets, single use, and inherited.
241. What is the recommended way to write a catch block?
Catch only exceptions you can actually handle, and catch the most specific type first, because a general clause earlier makes later ones unreachable. Handle means recover, translate, or add context; otherwise let the exception propagate. Log at the boundary where you stop the propagation, not at every level.
242. Why is catching Exception generally discouraged?
catch (Exception) also swallows OutOfMemoryException, StackOverflowException-adjacent failures, and programming bugs such as NullReferenceException, hiding defects and leaving the process in an unknown state. It is acceptable only at a top-level boundary that logs and terminates or returns a controlled error. Never catch and ignore silently.
243. How should a custom exception be designed?
Derive from Exception or a meaningful framework base, keep the three standard constructors, mark it [Serializable] when you need cross-process scenarios, and add only properties that carry useful context. Name it with an Exception suffix and make it immutable. Avoid deep hierarchies; one exception type per failure category is usually enough.
public sealed class InsufficientFundsException : Exception
{
public InsufficientFundsException(decimal requested, decimal available)
: base($"Requested {requested} but only {available} is available.")
{
Requested = requested;
Available = available;
}
public decimal Requested { get; }
public decimal Available { get; }
}
244. What is an inner exception used for?
An inner exception preserves the original cause when you wrap it in a more meaningful exception, so diagnostics keep the full chain instead of only the outer message. Always pass it to the constructor and prefer throw new MyException("context", ex) over rethrowing. Logging tools print the inner chain, which is often where the real root cause lives.
245. What is the difference between throw; and throw ex;?
throw; rethrows the current exception and preserves its original stack trace, while throw ex; restarts the trace at the current line, destroying the evidence of where the failure actually happened. Inside a catch block, always use bare throw; unless you intentionally wrap. Wrapping with a new exception that carries ex as inner is the other correct option.
246. Does finally run if the try block returns or throws?
Yes, finally runs on normal completion, on return, on break/continue, and on an unhandled exception as it leaves the block. A return inside finally is legal but overrides the pending result and swallows exceptions, so it should never be used. Environment.FailFast and process termination are the main cases where it does not run.
247. What are exception filters and when are they useful?
Filters use catch (Exception ex) when (condition) to decide whether a handler applies before the stack is unwound, so if the condition is false the exception continues with its original trace intact. They are useful for inspecting error codes or state without catching and rethrowing. Filter expressions should be side-effect free and fast.
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return null; // treat only 404 as "absent"
}
248. Are exceptions expensive in .NET?
Throwing is expensive because the runtime captures a stack trace, unwinds frames, and allocates the exception object, and it prevents some optimizations in the containing method. Catching a thrown exception is far costlier than a normal branch. The cost is acceptable for genuinely exceptional conditions but not for expected, frequent outcomes.
249. Should exceptions be used for normal control flow?
No. Using exceptions for expected outcomes such as a failed lookup makes code slower, harder to read, and harder to debug because the debugger breaks on every throw. Prefer TryParse-style patterns, nullable returns, or result types for expected failures. Reserve exceptions for violated preconditions and truly exceptional states.
250. Where should exceptions be logged and rethrown?
Log once, at the layer that makes the decision: an infrastructure boundary, a message handler, or the application entry point. Intermediate layers should add context and rethrow with throw; so the stack trace stays intact. Logging at every level produces duplicate noise and inflates log volume.
251. When would you use try/finally instead of using?
using is the right choice whenever the resource implements IDisposable, because it is exception-safe and concise. Use an explicit try/finally for cleanup that is not an IDisposable call, such as resetting a flag, releasing a lock by hand, or restoring ambient state. Both compile to the same shape when a using is present.
252. What does immutability mean for a type?
An immutable type cannot change after construction: all state is set in the constructor and exposed read-only. This makes instances inherently thread-safe, safe to use as dictionary keys, and easy to reason about. Reusable types often combine immutable state with methods that return new instances.
public sealed class Temperature
{
public Temperature(double celsius) => Celsius = celsius;
public double Celsius { get; }
public Temperature WarmerBy(double d) => new(Celsius + d);
}
253. What are the trade-offs of immutability?
Immutability removes whole classes of concurrency and aliasing bugs and makes equality and caching trivial, but copying large objects on every change costs allocations and memory. Persistent collections and builders mitigate that by sharing structure. Use immutability for value-like objects and small messages, and measure before making large graphs immutable.
254. What is the difference between a record, a class, and a struct?
A class is a reference type with reference equality by default; a struct is a value type copied on assignment with field-wise value equality; a record is a reference type that adds value equality, a readable ToString, with expressions, and deconstruction. Records express data, classes express behavior and identity, and structs suit small, short-lived values. A record struct combines record semantics with value-type storage.
255. When should you use a record struct instead of a record?
Use record struct for small data values where avoiding heap allocation matters, and prefer readonly record struct to keep it immutable. Because structs are copied, large instances become expensive to pass around, so keep them small. A reference-type record is the safer default when the type is used polymorphically or is larger than a few fields.
256. How does == differ from Equals for strings?
For string, == is overloaded to compare values using ordinal (case-sensitive, culture-insensitive) comparison, so it matches Equals(string) behavior. Both differ from culture-aware comparison, which is what you want for user-facing sorting rather than equality. For explicit intent, call string.Equals(a, b, StringComparison.Ordinal).
257. Why is comparing strings with ToLower() a bad idea?
ToLower() allocates a new string on every comparison and depends on the current culture, which can produce different results on different machines; the Turkish "i" is the classic case. Allocating also turns an O(1) comparison into extra GC pressure inside loops. Use StringComparison.OrdinalIgnoreCase or OrdinalIgnoreCase on a comparer instead.
258. When should you use Ordinal versus culture-aware string comparison?
Use Ordinal or OrdinalIgnoreCase for identifiers, keys, protocol values, file paths, and any programmatic equality, because it is fast and stable across cultures. Use CurrentCulture for sorting or comparing text displayed to users, where linguistic rules matter. Use InvariantCulture only for culture-independent, non-linguistic formatting and parsing.
259. Why are strings immutable, and what is interning?
String immutability makes them safe to share, hash, and use as dictionary keys, and allows the runtime to reuse literals. The CLR interns compile-time literals in a pool so identical literals are the same instance, which is why ReferenceEquals("a", "a") can be true. Concatenations and runtime-built strings are not interned, so never rely on reference equality for strings.
260. What are the main pitfalls with DateTime?
DateTime carries a Kind that is often ignored, so mixing Now (local) and UtcNow silently produces wrong offsets. Store and transmit UTC, convert to local only for display, and be explicit about DateTimeKind. Prefer DateTimeOffset when the original offset must be preserved.
261. When should you use DateTimeOffset instead of DateTime?
DateTimeOffset stores the UTC instant plus the original offset, so it round-trips the wall-clock time the user saw. Use it for timestamps that cross time zones or must be displayed as originally recorded, such as event schedules. DateTime in UTC is fine for pure storage and duration math.
262. Why should durations be stored as TimeSpan rather than DateTime?
TimeSpan represents an elapsed interval and has no calendar semantics, so adding and subtracting is unambiguous. DateTime arithmetic can be misleading across daylight-saving transitions and month lengths. Also be aware that DateTime.AddMonths is calendar-aware while AddDays(30) is not the same as adding a month.
263. When should you use decimal instead of double?
Use decimal for money and any value where exact base-10 representation matters, because it avoids binary rounding errors such as 0.1 + 0.2 not equalling 0.3. double is faster and has a much wider range, so it suits scientific and geometric calculations. Never compare floating-point values with ==; compare against a tolerance.
Console.WriteLine(0.1 + 0.2 == 0.3); // false
Console.WriteLine(0.1m + 0.2m == 0.3m); // true
Console.WriteLine(Math.Abs(a - b) < 1e-9); // tolerant compare
264. When is float or double the correct choice?
Choose them for physical measurements, graphics, statistics, and machine learning where approximate values and speed matter more than exact decimal digits. float halves the memory of double and is common in GPU and large-array scenarios. Avoid both for currency and for identifiers you must compare exactly.
265. What do checked and unchecked do?
checked makes integral overflow and invalid conversions throw OverflowException; unchecked silently truncates or wraps. C# is unchecked by default at runtime, though constant expressions are checked at compile time. You can enable project-wide checking with <CheckForOverflowUnderflow> and use checked blocks for risky arithmetic.
int max = int.MaxValue;
int wrapped = unchecked(max + 1); // int.MinValue
int boom = checked(max + 1); // throws
266. What happens silently when arithmetic overflows by default?
The value wraps around modulo 2^n, so a large positive number can become negative without any error, which is a subtle source of financial and index bugs. Division by zero for integers still throws DivideByZeroException regardless of the checked context. Use checked, wider types, or explicit range validation in code that must be correct.
267. When should you use StringBuilder instead of string concatenation?
Because strings are immutable, each concatenation allocates a new string and copies both operands, making a loop quadratic. StringBuilder mutates an internal buffer with amortized append cost, so use it for loops and dynamic building. For a fixed, small number of concatenations the compiler's string.Concat or interpolation is already optimal.
var sb = new StringBuilder(capacity: 256);
foreach (var row in rows) sb.Append(row).Append('\n');
string text = sb.ToString();
268. What is nameof for?
nameof returns the simple name of a variable, type, or member as a compile-time constant string, so refactoring renames it automatically. It is ideal for argument exceptions, property change notifications, and logging. There is no runtime cost because the value is baked into the assembly.
if (value is null) throw new ArgumentNullException(nameof(value));
269. What does the default literal do?
default produces the default value of the target type: null for reference types, zero for numeric types, and a zeroed value for structs. Target-typed, it avoids repeating the type name, so CancellationToken token = default; is idiomatic. For generic code, default! may be needed to satisfy nullable analysis when T is unconstrained.
270. What is the is not pattern?
is not is a negated pattern test that reads better than !(x is T) and works with type, constant, property, and relational patterns. It also enables the idiomatic guard if (value is not null). Combined with pattern variables it keeps the value strongly typed after the check.
if (shape is not Circle { Radius: > 0 })
throw new ArgumentException("Expected a positive circle.", nameof(shape));
271. What is a switch expression?
A switch expression returns a value, uses pattern-based arms, and requires exhaustiveness, so the compiler warns when a case is missing. It replaces verbose switch statements with assignment and is expression-bodied and immutable-friendly. Use _ as the discard arm for the default case.
string label = status switch
{
Status.New => "new",
Status.Paid => "paid",
Status.Cancelled => "cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status))
};
272. What is target-typed new?
new() without a type name infers the type from the declaration or argument position, reducing repetition such as List<Order> orders = new();. It is compile-time only, so readability depends on the declared type being visible nearby. Avoid it when the target type is far away or ambiguous to a reader.
273. What is the difference between const and static readonly?
const is a compile-time literal baked into every consuming assembly, so changing it requires recompiling all callers; it is limited to primitives, strings, and null. static readonly is evaluated once at type initialization and is a runtime field, so it can hold any type and object references. Use const for true constants and static readonly for values computed at startup.
274. What is an extension method?
An extension method is a static method whose first parameter is marked this, allowing it to be called with instance syntax on that type. It must live in a static, non-nested, non-generic class and be visible via a using directive. It cannot access private members, so it extends the public surface only.
public static class StringExtensions
{
public static bool IsBlank(this string? s) =>
string.IsNullOrWhiteSpace(s);
}
275. How does the compiler choose between an instance method and an extension method?
Instance methods always win: an extension method is only considered when no applicable instance member exists. Among extensions, the best match by argument types wins, with closer namespaces and fewer using hops preferred over those from far-away namespaces. If two candidates are equally good, the call is ambiguous and does not compile.
276. Can an extension method override a virtual method or be dispatched polymorphically?
No. Extension methods are static and resolved from the compile-time type, so they never participate in virtual dispatch and cannot be overridden. A derived class declaring an instance method with the same name will hide the extension for callers typed as the derived class. If polymorphism is needed, use an interface or virtual member instead.
277. What is array covariance?
Arrays of reference types are covariant: string[] can be assigned to object[] because string derives from object. This is checked at runtime, not compile time, so writing an incompatible element throws ArrayTypeMismatchException. Value-type arrays are not covariant, so int[] cannot be treated as object[].
object[] items = new string[1];
items[0] = 42; // compiles, throws ArrayTypeMismatchException
278. Why did array covariance turn out to be a design flaw?
It trades compile-time safety for compatibility with Java-style APIs, deferring a detectable type error to runtime. Every array write must therefore be type-checked, which costs performance on hot paths. Generic collections are invariant precisely to avoid this hole.
279. Why can't you pass a List<Derived> where List<Base> is expected?
Generic type arguments are invariant, because List<T> is mutable: the callee could add a Base that is not a Derived, corrupting the caller's list. Arrays allow it and pay for it with runtime checks. Use interfaces with out T such as IEnumerable<Base>, or project with Cast<Base>() to get a safe read-only view.
280. What are the main differences between a struct and a class?
A struct is a value type: it is copied on assignment, cannot inherit or be inherited, cannot be null unless nullable, and gets field-wise value equality by default. A class is a reference type with reference semantics, inheritance, and a finalizer option. Structs are good for small, immutable, short-lived data; classes for identity and behavior.
Intermediate 340 questions
Async and concurrency, language depth, dependency injection, and ASP.NET Core: routing, binding, validation, filters, auth, REST design.
281. What does the compiler actually generate when you mark a method async?
It generates a nested struct (or class) that implements IAsyncStateMachine, plus a stub method that creates and starts it. The state machine stores local variables as fields so they survive suspension, and it uses an AsyncTaskMethodBuilder to create the returned Task and to complete it later. Each await point becomes a state number plus a call to AwaitUnsafeOnCompleted that registers a continuation. When the awaited operation finishes, MoveNext is re-entered, the state number routes execution to the right place, and locals are restored from fields.
[AsyncStateMachine(typeof(<GetAsync>d__1))]
public Task<string> GetAsync() {
var sm = new <GetAsync>d__1();
sm.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
sm.<>1__state = -1;
sm.<>t__builder.Start(ref sm);
return sm.<>t__builder.Task;
}
282. Does async/await create a new thread?
No. async/await is a scheduling construct, not a threading construct: the method runs on the caller's thread until the first incomplete await, and the continuation runs on whatever thread the awaited operation's scheduler provides. For I/O work, no thread is blocked while the operation is in flight, which is why async scales better than a blocking call on a thread pool thread. Extra threads appear only when the continuation has to be posted back to a specific context (UI, legacy ASP.NET) or when you explicitly call Task.Run.
283. When would you choose ValueTask over Task?
Use ValueTask when the operation frequently completes synchronously and is called in hot paths, because it avoids a heap allocation for the Task object. The benefit only materialises if you also avoid allocation elsewhere, so it is a micro-optimisation for measured hotspots such as a caching layer or a parser. Never use it when the result must be awaited multiple times, stored, or consumed concurrently, and never block on it with .Result.
public ValueTask<int> ReadAsync(Memory<byte> buffer) {
if (_position < _cached.Length)
return new ValueTask<int>(Copy(_cached, buffer));
return ReadSlowAsync(buffer);
}
284. Why is it unsafe to await a ValueTask more than once?
A ValueTask is a struct that may wrap either a result or a pooled IValueTaskSource, and the underlying source is recycled by the producer as soon as it is consumed. A second await can therefore observe corrupted state or an already-released object, and concurrent awaits are outright undefined behaviour. If you need multi-consumption semantics, call AsTask() once to get a regular Task and await that.
ValueTask<int> vt = ReadAsync(buf);
Task<int> t = vt.AsTask(); // safe to await many times
int a = await t, b = await t;
285. What exactly does ConfigureAwait(false) change?
It tells the awaiter not to capture the current SynchronizationContext or TaskScheduler, so the continuation may run on any thread pool thread instead of being marshalled back. It does not change which thread starts the method, and it does not prevent the continuation from running on the original thread by coincidence. The only guarantee is the absence of a forced post back.
286. Do you still need ConfigureAwait(false) in ASP.NET Core application code?
In ASP.NET Core there is no SynchronizationContext, so ConfigureAwait(false) is a no-op for deadlock avoidance and is mostly noise in application code. It still matters in reusable libraries, which may be consumed by WPF, WinForms, or legacy ASP.NET where a context exists. A common pragmatic rule is: use it in library code, skip it in app code unless you rely on the minor scheduling optimisation.
287. How should a CancellationToken be propagated through an async call chain?
Accept a CancellationToken as the last parameter of every async method that can be cancelled, give it a default only on public entry points, and pass the same token down to every awaitable call. Never wrap it in a new CancellationTokenSource unless you genuinely need to combine or time-limit the operation. Do not store the token in a field or a singleton, since it is tied to one logical operation.
public async Task<Report> BuildAsync(int id, CancellationToken ct) {
var data = await _repo.LoadAsync(id, ct);
await _cache.SetAsync(id, data, ct);
return Map(data);
}
288. How do you implement a timeout that also cancels the underlying work?
Create a linked token source with CancelAfter, pass its token to the operation, and translate the resulting OperationCanceledException into a timeout result when the timeout token is the one that fired. In .NET 8 and later, CancellationTokenSource.CancelAfter plus checking ct.IsCancellationRequested is the idiomatic approach; Task.WaitAsync(TimeSpan) is a shortcut when you only need to stop waiting and are willing to let the underlying work continue.
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(3));
try { return await _client.GetAsync(url, cts.Token); }
catch (OperationCanceledException) when (!ct.IsCancellationRequested) {
throw new TimeoutException("Upstream call timed out.");
}
289. What is the difference between Task.WhenAll and Task.WhenAny?
WhenAll returns a single task that completes when every input task has completed, which makes it the tool for parallel fan-out where all results are needed. WhenAny returns a task whose result is the first input task to finish, which is the tool for timeouts, racing replicas, or reacting to whichever completes first. Both are non-blocking, unlike WaitAll and WaitAny, and neither starts the tasks for you.
290. How does Task.WhenAll report multiple failures?
The returned task faults with the first exception and exposes all failures through its Exception.InnerExceptions collection, so awaiting it with a plain await rethrows only the first one. If you catch, inspect task.Exception or catch the AggregateException explicitly to see every failure. Tasks that never faulted still completed their work, so partial side effects may already have happened.
var tasks = ids.Select(id => _svc.ProcessAsync(id));
try { await Task.WhenAll(tasks); }
catch {
var errors = tasks.Where(t => t.IsFaulted)
.SelectMany(t => t.Exception!.InnerExceptions);
_log.LogError("Failures: {Count}", errors.Count());
}
291. How do you await a heterogeneous set of tasks and collect all results?
Start the tasks first, then await each one individually; awaiting sequentially is fine because the work is already running concurrently. This gives type-safe results without casting and preserves per-call error handling. Task.WhenAll is a poor fit when the result types differ.
var userTask = _users.GetAsync(id, ct);
var ordersTask = _orders.ForUserAsync(id, ct);
var flagsTask = _flags.GetAsync(id, ct);
var user = await userTask;
var orders = await ordersTask;
var flags = await flagsTask;
292. Where does an exception thrown inside an async method surface?
It is captured and stored on the returned Task, then rethrown at the await site with its original stack trace preserved via ExceptionDispatchInfo. Code before the first await still throws synchronously if the method is not async, but an async method always returns a faulted task instead. Using await rather than .Wait or .Result preserves the original exception type instead of wrapping it in AggregateException.
293. Why are async void methods dangerous?
An async void method returns nothing to await, so the caller cannot know when it finishes or whether it failed. Exceptions escape to the SynchronizationContext or the thread pool and typically crash the process, and the method cannot be composed, retried, or cancelled by the caller. Use async Task everywhere except UI event handlers.
294. If async void is so risky, why is it allowed at all?
It exists for the event handler pattern, where a delegate signature such as EventHandler is fixed to void and cannot be changed by the subscriber. The acceptable pattern is a thin async void handler that immediately delegates to an async Task method and catches everything, so nothing escapes. In libraries, offer an async Task overload instead of an async void API.
private async void OnSaveClicked(object? s, EventArgs e) {
try { await SaveAsync(); }
catch (Exception ex) { ShowError(ex); }
}
295. How does await foreach work with IAsyncEnumerable?
The compiler calls GetAsyncEnumerator, then awaits MoveNextAsync before each iteration and disposes the enumerator asynchronously at the end. This lets the producer yield items as they arrive, for example from a database reader or a paginated API, without buffering the whole sequence. A CancellationToken can be passed via WithCancellation and is checked by the producer.
await foreach (var row in _source.ReadRowsAsync(ct).WithCancellation(ct)) {
Process(row);
}
296. What does WithCancellation on IAsyncEnumerable actually do?
It attaches the token to the enumerable so that the enumerator receives it in GetAsyncEnumerator(cancellationToken) and can observe it in MoveNextAsync. The producer must honour the token; WithCancellation alone guarantees nothing. It is also the standard way to flow the caller's cancellation into an await foreach loop.
297. Can you control buffering when consuming an IAsyncEnumerable?
Yes, via System.Linq.Async or the built-in ChannelReader.ReadAllAsync plus operators such as WithCancellation; some providers expose options for batch size and prefetch. On the producing side, you decide the buffering by how many items you yield ahead of consumption, and a bounded channel is the usual way to apply backpressure. There is no global buffer setting on the interface itself.
298. Why does calling .Result on an async method deadlock in some applications?
The synchronous caller blocks its thread while holding the SynchronizationContext, and the async continuation is queued back to that same single-threaded context, which can never run because the thread is blocked. UI applications and classic ASP.NET exhibit this. Fixing it means making the call chain async all the way, and never mixing blocking waits with async code.
// Deadlock in a UI app: the context is captured and blocked
var data = GetDataAsync().Result;
// Correct
var data = await GetDataAsync();
299. There is no SynchronizationContext in ASP.NET Core, so is .Result safe there?
It will not deadlock the same way, but it is still harmful: it blocks a thread pool thread, can starve the pool under load, and wraps exceptions in AggregateException. Under legacy thread-affinity assumptions or inside code that does need the context, it can still hang. Treat sync-over-async as a defect regardless of the hosting model.
300. Compare Thread, ThreadPool and Task.
A Thread is a dedicated OS thread with full control over apartment state and priority, which is rarely needed and expensive to create. The ThreadPool is a shared pool of worker threads suited to short CPU-bound work, with heuristics that inject threads slowly when the queue is starved. Task is a promise of future completion scheduled on the pool by default, composing cleanly with async I/O and continuations, which makes it the default choice for both I/O and parallelism.
301. What are the symptoms of thread pool starvation and how do you fix it?
Latency climbs while CPU stays low, throughput collapses, and the pool injects a new thread roughly every 500 ms, so requests queue behind blocking calls. The usual cause is sync-over-async or long blocking work on pool threads. Fix it by making the path truly async, moving long blocking work to dedicated threads or a queue, and using the ThreadPool starvation metrics available in .NET 6+ to confirm.
302. Compare lock, Monitor, Mutex and SemaphoreSlim.
lock is syntactic sugar for Monitor.Enter/Exit and provides mutual exclusion within a single process with a highly optimised fast path. Mutex is a kernel object that can be system-wide and named, at the cost of much higher overhead, and it is thread-affine. SemaphoreSlim is a lightweight in-process counter that limits concurrency, supports WaitAsync, and is the right primitive when you need an async-compatible lock.
303. Why can't you await inside a lock statement?
Monitor is thread-affine: the lock must be released by the same thread that acquired it, but an await can resume on a different thread and the compiler forbids the construct outright. Even if it were allowed, holding a lock across an I/O await would block every other caller for the entire duration. Use SemaphoreSlim.WaitAsync or restructure so the critical section contains no awaits.
304. How do you implement an async lock?
Use SemaphoreSlim with an initial count of one and always release it in a finally block so an exception cannot leak the permit. Dispose the semaphore when the owning object is disposed, and never re-enter it reentrantly, because a SemaphoreSlim is not reentrant and will deadlock.
private readonly SemaphoreSlim _gate = new(1, 1);
public async Task<T> RunAsync<T>(Func<Task<T>> work, CancellationToken ct) {
await _gate.WaitAsync(ct);
try { return await work(); }
finally { _gate.Release(); }
}
305. When is Interlocked enough instead of a lock?
Interlocked is enough for single-variable atomic operations such as increment, exchange, compare-and-swap and additive updates, and it is lock-free and fast. As soon as the invariant spans two variables or requires a read-modify-write across multiple steps, you need a lock or an immutable snapshot. Interlocked on a long is atomic on 64-bit platforms, but compound invariants still need protection.
Interlocked.Increment(ref _counter);
Interlocked.CompareExchange(ref _state, Running, Idle);
306. What is the difference between volatile, Interlocked and lock?
volatile only prevents reordering and caching of a single field's reads and writes, providing visibility but no atomicity for compound operations. Interlocked provides atomic read-modify-write with full memory barriers for one location. lock provides mutual exclusion for a whole critical section and also acts as a full fence, which is what you need when several fields must change together.
307. What is the classic pitfall with ConcurrentDictionary.GetOrAdd?
The value factory is not guaranteed to run only once: two threads can both miss and both invoke it, and only one result is stored. That is harmless for pure factories but dangerous when the factory performs I/O, registers a subscription, or has side effects. Use Lazy<T> as the value type when exactly-once initialisation is required.
var lazy = _cache.GetOrAdd(key,
k => new Lazy<Client>(() => CreateClient(k), LazyThreadSafetyMode.ExecutionAndPublication));
var client = lazy.Value;
308. Are compound operations on ConcurrentDictionary atomic?
Each individual method is thread-safe, but a sequence of them is not, so read-then-write patterns have races. AddOrUpdate, GetOrAdd and TryUpdate are atomic per key, but conditions spanning multiple keys or a check-then-act across calls need external synchronisation. Also remember that enumeration is a moving snapshot and does not lock the dictionary.
309. What are Channels and when do you use them?
System.Threading.Channels is a modern async producer/consumer queue with a single-reader or multi-reader and single-writer or multi-writer configuration. Producers call WriteAsync, consumers await ReadAllAsync or ReadAsync, and a bounded capacity applies backpressure instead of unbounded memory growth. It is the default choice for in-process pipelines, background queues, and decoupling ingestion from processing.
var ch = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(1000) {
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true
});
await ch.Writer.WriteAsync(item, ct);
await foreach (var i in ch.Reader.ReadAllAsync(ct)) Handle(i);
310. How does a consumer know a channel has no more data?
The producer calls Writer.Complete(), which signals completion; ReadAllAsync then ends gracefully after draining remaining items, and a direct ReadAsync throws ChannelClosedException. Always complete the writer in a finally block so consumers do not hang forever when a producer faults. Completion can carry an exception that surfaces to the reader.
311. Why prefer Channel over BlockingCollection?
BlockingCollection is synchronous, so an async producer must block a thread to add items, and it relies on blocking waits that do not scale. Channels expose WriteAsync/ReadAsync and integrate naturally with cancellation and await foreach, and they have lower overhead and finer configuration. BlockingCollection remains fine for purely synchronous legacy pipelines.
312. How does Parallel.ForEachAsync differ from Parallel.ForEach?
Parallel.ForEachAsync takes an async body with a CancellationToken and honours a MaxDegreeOfParallelism limit, awaiting each asynchronous iteration instead of blocking a thread per item. Parallel.ForEach is for CPU-bound synchronous work and will simply block threads on async delegates, which defeats the purpose. ForEachAsync is the correct tool for a bounded fan-out of I/O calls.
await Parallel.ForEachAsync(ids, new ParallelOptions {
MaxDegreeOfParallelism = 8, CancellationToken = ct
}, async (id, token) => await _svc.SyncAsync(id, token));
313. When is PLINQ a good idea?
PLINQ pays off for CPU-bound work over large in-memory collections where each element is expensive enough to dwarf the partitioning and merging overhead. It is a poor fit for I/O-bound work and for small collections, where the parallel overhead dominates. Use WithDegreeOfParallelism and AsOrdered deliberately, and remember that exceptions surface as an AggregateException.
314. What are the trade-offs of AsOrdered and AsUnordered in PLINQ?
AsOrdered preserves source order at the cost of extra buffering and coordination, reducing speedup. AsUnordered removes that constraint and lets results stream as they complete, which is faster but nondeterministic. Only ask for ordering if downstream logic actually depends on it, and consider ordering once at the end instead of after every operator.
315. How do you choose between Channel, BlockingCollection and TPL Dataflow for a producer/consumer design?
Use a Channel for a simple async pipeline with backpressure and minimal ceremony, which covers most application needs. BlockingCollection suits synchronous code that already blocks threads. TPL Dataflow is the right choice when you need a graph of blocks with different parallelism, batching, joining and completion propagation, at the cost of more complexity.
316. What is the difference between System.Threading.Timer, System.Timers.Timer and PeriodicTimer?
System.Threading.Timer is the lowest-level pool-based callback timer with no UI marshalling. System.Timers.Timer wraps it with events and an AutoReset option, mainly for legacy component use. PeriodicTimer (since .NET 6) is the async-friendly choice: await WaitForNextTickAsync in a loop, so ticks cannot overlap and cancellation is natural.
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
await PollAsync(ct);
317. Why can callbacks from System.Threading.Timer overlap?
The timer queues a callback on the thread pool every period regardless of whether the previous callback finished, so a slow callback overlaps with the next one. Guard against reentrancy with a flag, an Interlocked exchange, or a SemaphoreSlim with wait-zero semantics, or switch to PeriodicTimer which serialises ticks by design. Overlapping callbacks also mean concurrent access to shared state.
318. What is TaskCompletionSource used for?
It lets you expose an existing callback-based or event-based API as an awaitable Task, bridging code that completes later into async code. You set the result, exception or cancellation exactly once, and the awaiter resumes accordingly. Always use RunContinuationsAsynchronously in library code to avoid running the awaiting continuation inline on the completing thread.
var tcs = new TaskCompletionSource<Message>(
TaskCreationOptions.RunContinuationsAsynchronously);
_connection.OnMessage += m => tcs.TrySetResult(m);
using var reg = ct.Register(() => tcs.TrySetCanceled(ct));
return await tcs.Task;
319. Why is fire-and-forget with Task.Run discouraged in ASP.NET Core?
The request-scoped services and HttpContext may be disposed while the background task is still running, and the task's exceptions are unobserved. There is also no backpressure, so a burst of requests can flood the thread pool. Use a hosted background service with a bounded queue or Channel instead.
320. What does AsyncLocal flow across and when does it not?
AsyncLocal values flow along async continuations and through ExecutionContext, so a value set before an await is visible after it, including in Task.Run work started from that context. They do not flow to threads started manually with new Thread, nor backwards to the parent, and mutations inside a child scope are not visible to the caller. This is the mechanism behind logging scopes and Activity correlation.
321. What is ExecutionContext.SuppressFlow for?
It prevents the current ExecutionContext, including AsyncLocal values such as Activity and logging scope, from being captured when starting a task or thread. Use it for background work that must not inherit request-scoped ambient state, and dispose the returned object promptly to restore flow. Suppressing flow can also improve throughput by avoiding context copies in very hot paths.
322. What is a SynchronizationContext and how does await interact with it?
It is an abstraction representing where a continuation should run, such as the UI thread or the classic ASP.NET request context. When you await, the awaiter captures SynchronizationContext.Current and posts the continuation there unless ConfigureAwait(false) suppresses it. ASP.NET Core installs no context, so continuations run on the pool.
323. What is the difference between Task.Yield and Task.Delay(0)?
Task.Yield forces an asynchronous yield, always producing an incomplete awaitable that schedules the rest of the method back onto the current context or scheduler, which is useful to keep a UI responsive. Task.Delay(0) returns an already-completed task, so awaiting it does not yield at all and the method continues synchronously. Use Yield for fairness, not Delay(0).
324. What are the risks of registering many callbacks on a CancellationToken?
Each registration allocates and is retained by the token source until disposed, so long-lived tokens with per-operation registrations leak memory and slow cancellation. Always dispose the registration, for example with a using on the returned CancellationTokenRegistration. For repeated operations, prefer a linked token created per operation and disposed with it.
using var reg = ct.Register(static s => ((Socket)s!).Dispose(), socket);
325. How do you safely convert a synchronous method into an async one without breaking callers?
Add an async path alongside the existing one, keeping the synchronous signature as a thin wrapper only for compatibility, and move I/O to truly asynchronous APIs rather than wrapping everything in Task.Run. Async-all-the-way is the goal because Task.Run over blocking I/O merely moves the blocked thread. Where a sync API must exist, document it and keep it out of ASP.NET Core request paths.
326. What kinds of generic constraints exist and what do they enable?
Constraints are class/struct/unmanaged/notnull, base class or interface constraints, new(), and since C# 11 the allows ref struct anti-constraint for generic math and span scenarios. Their real value is that they let the compiler bind members at compile time, avoiding boxing and runtime casts. Prefer the narrowest constraint that satisfies the code, because every constraint narrows the API's usability.
public static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
327. What does the new() constraint require and what are its limits?
new() requires a public parameterless constructor and lets you write new T(), which is essential for factories and generic pools. It excludes abstract types and interfaces, and it cannot be combined with struct in the same constraint list. For types with private constructors you must fall back to Activator.CreateInstance or a factory delegate.
328. How does generic math work in modern .NET?
Static abstract members in interfaces allow operators to be expressed as constraints, so a single generic method can add or compare any numeric type without boxing or dynamic. INumber<T> unifies the numeric types and enables algorithms written once for int, double, decimal or a custom type. The JIT can inline the static calls, so performance matches hand-written overloads.
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T> {
T total = T.Zero;
foreach (var v in values) total += v;
return total;
}
329. What do out and in mean on a generic type parameter?
out declares covariance: the type parameter appears only in output positions, so IEnumerable<string> can be used as IEnumerable<object>. in declares contravariance: it appears only in input positions, so IComparer<object> can be used as IComparer<string>. Variance is only allowed on interfaces and delegates, and only for reference types.
330. Why is List<object> not assignable to List<string> even though object is a base type?
Generic classes are invariant, so List<T> exposes T in both input and output positions and the compiler cannot guarantee safety. Allowing the assignment would let you add an object that is not a string and later read it as one. Interfaces such as IEnumerable<out T> and IReadOnlyList<out T> express covariance precisely because they are read-only.
331. How does variance apply to delegates such as Func and Action?
Func<in T, out TResult> is contravariant in its input and covariant in its result, so a Func<object, string> can be used where Func<string, object> is expected. Action<in T> is contravariant only. This is what allows a handler written for a base EventArgs to be registered for a derived event.
Func<object, string> f = o => o.ToString()!;
Func<string, object> g = f; // valid: in/out variance
332. What is an expression tree and what makes it different from a delegate?
An Expression<TDelegate> is a data structure describing code as nodes rather than compiled instructions, so a consumer can inspect, rewrite or translate it. Delegates are opaque executable code and cannot be analysed. Expression trees are the basis of IQueryable providers, ORMs, and dynamic rule engines, and they can be turned into delegates with Compile().
333. When is Expression<Func<T,bool>> required instead of Func<T,bool>?
When the consumer must translate the predicate instead of executing it, which is exactly the IQueryable case where EF Core converts nodes into SQL. Passing a Func to a queryable overload silently switches to client evaluation or fails to compile. Conversely, use a plain Func inside in-memory LINQ to avoid the translation and compilation cost.
334. What kinds of code break EF Core query translation?
Custom methods with no translator, arbitrary string manipulation, dynamic or reflection-based expressions, and some date/time and collection operations either throw or force client evaluation. The reliable pattern is to keep the predicate translatable and move anything else after AsEnumerable or after the final projection. Always check the generated SQL for unexpected client-side evaluation.
335. What is the cost of Expression.Compile and how do you mitigate it?
Compile emits IL at runtime, which is expensive relative to normal method calls, so compiling per request is a real cost. Cache the compiled delegate keyed by the expression, or use EF Core's compiled queries for hot paths. Compile(preferInterpretation: true) can be faster for one-off use where JIT cost dominates.
336. How does IQueryable differ from IEnumerable?
IEnumerable represents a sequence you enumerate in memory, so every operator runs as CLR code. IQueryable carries an expression tree plus a provider, so operators build a query that the provider translates, typically into SQL executed remotely. The distinction decides whether filtering happens in the database or after materialising everything.
337. Why is deferred execution a source of bugs?
A LINQ query is a recipe, not a result: it re-executes every time it is enumerated, so the data may have changed and side effects may run repeatedly. Enumerating the same query twice in one method means two database round-trips. Materialise with ToList when you need a stable snapshot or will iterate more than once.
var q = ctx.Orders.Where(o => o.Total > 100);
var count = q.Count(); // query 1
var list = q.ToList(); // query 2, possibly different data
338. Where exactly should AsEnumerable be placed in a query?
Place it after the last operation that can be translated and before operations that cannot, so the database does the filtering and projection and only the reduced result set is processed in memory. Placing it too early pulls the whole table into memory. AsNoTracking is a separate concern and does not affect where execution happens.
339. What is the difference between tracking and no-tracking queries in EF Core?
Tracking queries register entities in the change tracker so updates are detected on SaveChanges, at the cost of identity resolution and memory. AsNoTracking is faster and cheaper for read-only endpoints and is safe when you never mutate or rely on identity fixup. Use AsNoTrackingWithIdentityResolution when you need one instance per key without change tracking.
340. What problem does Span<T> solve?
Span<T> is a stack-only view over contiguous memory, whether an array, stack memory or unmanaged buffer, so you can slice and parse without allocating substrings or copies. It unifies the API surface for string, array and native memory processing. It is a ref struct, so it cannot be boxed, stored on the heap, or used across await.
ReadOnlySpan<char> span = "2026-10-22".AsSpan();
var year = span[..4];
if (int.TryParse(year, out var y)) Console.WriteLine(y);
341. Why does Memory<T> exist when Span<T> is faster?
Memory<T> is a heap-friendly view that can be stored in fields, captured by lambdas and used across await, then converted to Span only when you actually process it. Async APIs therefore accept Memory<T> while synchronous hot paths accept Span<T>. The conversion is cheap but must not outlive the await.
public async Task ReadAsync(Memory<byte> buffer, CancellationToken ct) {
await _stream.ReadAsync(buffer, ct);
var span = buffer.Span; // valid only for this synchronous block
Process(span);
}
342. Why can't a Span be used inside an async method?
Span is a ref struct that may point at stack memory, and the compiler must guarantee it never escapes to the heap; an async state machine lifts locals into a heap object across awaits, which would break that guarantee. The same restriction applies to capturing a Span in a lambda or using it as a generic type argument. Use Memory<T> or a scoped synchronous helper instead.
343. How do you use ArrayPool correctly?
Rent a buffer sized to your need, use only the requested length, and always return it in a finally block with the exact same array. Do not return a different array, do not keep references after returning, and clear only when the buffer held sensitive data. Pooling removes GC pressure for large transient buffers but adds correctness risk if the return is skipped.
var buf = ArrayPool<byte>.Shared.Rent(4096);
try { var read = await s.ReadAsync(buf.AsMemory(0, 4096), ct); Use(buf, read); }
finally { ArrayPool<byte>.Shared.Return(buf); }
344. What are the practical limits of a ref struct?
It cannot be boxed, stored in a field of a class, used as a generic type argument (before the allows ref struct anti-constraint), captured in a lambda, or used across await or yield. Those restrictions are exactly what allow the runtime to guarantee it never escapes the stack. Use them for short-lived, high-performance buffer code.
345. When is stackalloc safe?
Only for small, fixed, bounded sizes, because stack space is limited (typically around 1 MB per thread) and an overflow terminates the process rather than throwing. Never stackalloc based on untrusted or unbounded input; threshold-check and rent from ArrayPool instead. Since C# 7.2 stackalloc can target Span<T>, which makes slicing and bounds checks safe.
Span<byte> buffer = stackalloc byte[256]; // small and constant
if (length > 256) return RentAndProcess(length);
346. What does the in modifier do on a parameter?
It passes a struct by reference read-only, avoiding the copy cost of a large struct while promising the callee will not mutate it. The compiler enforces immutability and may create a defensive copy when the argument is not readonly. Use it for structs larger than roughly a couple of machine words on hot paths.
public static double Length(in Vector3 v) => Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
347. What is the difference between ref readonly return and in parameter?
ref readonly on a return exposes a reference to existing storage that callers can read but not write, avoiding a copy of a large struct. in restricts the callee instead, promising not to modify the argument. Both are performance tools that leak implementation details about storage lifetime, so use them sparingly and document them.
348. How do you decide between a struct and a class?
Choose a struct for small, immutable, value-semantic data that is short-lived and not frequently boxed, such as coordinates, money or identifiers. Choose a class when you need identity, inheritance, nullability, or the object is large or long-lived, since copying a big struct is expensive. Never make a mutable struct, because copies are easy to create accidentally.
349. What do readonly struct and readonly members protect against?
A readonly struct guarantees no field can be mutated after construction, which removes defensive copies when it is passed as in or accessed through a readonly reference. Without readonly, the compiler copies the struct to call a non-readonly member on a readonly location, silently hurting performance. This is why immutable structs should be declared readonly.
350. What are the equality semantics of a record?
Records generate value equality over all instance fields and properties, plus consistent GetHashCode, == and != operators, ToString and Deconstruct. Equality uses the runtime type, so a record and its derived record with the same values are not equal. Use record struct for a value type with the same generated members.
351. How do init accessors and with expressions work together?
init allows a property to be set only during object initialisation or from a constructor, making immutability the default after construction. The with expression performs a shallow copy and then applies the listed changes, which is how you produce a modified immutable value. Because the copy is shallow, nested reference members are still shared with the original.
var a = new Order(Id: 1, Total: 100m);
var b = a with { Total = 120m }; // a is unchanged
352. What do required members add over a constructor?
required forces callers to set a property in an object initialiser or via SetsRequiredMembers, and it works with object initialisers and JSON deserialisation where a constructor cannot express every combination. It removes the need for dozens of constructor overloads while keeping the compiler's guarantee. The setter still exists, so it protects against omission, not against later mutation.
353. How do primary constructors on a class differ from those on a record?
On a record, primary constructor parameters become public init-only properties and participate in equality. On a plain class, the parameters are just captured values available to the body; they are not properties unless you declare them, so they are not serialised or compared. This makes class primary constructors useful mainly for dependency injection without boilerplate.
public sealed class Orders(IOrderRepo repo, ILogger<Orders> log) {
public Task<Order?> GetAsync(int id) => repo.FindAsync(id);
}
354. What is a property pattern and when does it simplify code?
A property pattern matches an object by testing members, optionally with nested patterns, and it replaces chains of if-statements with casting and null checks. It is null-safe: if the value is null the pattern simply does not match. It reads well for decision logic over DTOs and domain results.
if (order is { Status: OrderStatus.Paid, Total: > 1000m })
FlagForReview(order);
355. How do positional patterns work?
A positional pattern deconstructs the value using an accessible Deconstruct method or the record-generated one, then matches each element. This makes records and tuples natural to match on and keeps the code declarative. Non-record types need a Deconstruct overload with out parameters to participate.
var label = point switch {
(0, 0) => "origin",
(var x, 0) => $"on X axis at {x}",
var (x, y) => $"({x},{y})"
};
356. What are list patterns used for?
List patterns match sequences by position, length and slice, so you can express validation of small collections without index arithmetic. They work on any type with a Length or Count and an indexer, and require the type to be countable and indexable. They are most useful for parsers, protocol headers, and argument validation.
string Describe(int[] v) => v switch {
[] => "empty",
[var only] => $"single {only}",
[var first, .., var last] => $"{first}..{last}"
};
357. Does a switch expression guarantee exhaustiveness?
Only for closed sets the compiler understands, such as enums and sealed hierarchies; otherwise you need a discard arm, and without one the compiler emits a warning and the code throws at runtime for unmatched values. A discard arm is also where you should throw rather than silently return a default. Non-exhaustive switches over enums are a common source of production surprises after adding a new member.
358. When would you use record struct instead of record class?
Use record struct for small, value-semantic data that benefits from avoiding heap allocation, such as keys, coordinates or small DTOs inside loops. Because records are copy-on-assign, a large record struct still pays copy costs. record class remains the default for domain entities with identity and reference semantics.
359. What is the purpose of static abstract interface members?
They let an interface declare static members that implementing types must supply, enabling generic code to call operators, Parse, or factory-like statics through a type parameter. This is how generic math works and how you can write algorithms constrained to "any type with a TryParse". Previously such patterns required reflection or a separate factory abstraction.
360. What are default interface methods and what problems do they introduce?
They let an interface provide a method body, so implementers do not have to, which enables evolving an interface without breaking every implementation. The catch is that the default is only reached through the interface, so a class method with the same name does not override it, and calling through the class type skips it. Multiple interfaces with conflicting defaults require an explicit implementation in the class.
361. How does the single responsibility principle look in real C# code?
It means one reason to change, so a class mixes fewer concerns: an OrderService orchestrates, a repository persists, a mapper translates, a validator validates. The tell is a class name with "and" in it or a test that needs a database to check formatting. Splitting too aggressively is also a smell, so group by axis of change rather than by line count.
362. Give a concrete violation of the Liskov substitution principle in C#.
A Square deriving from Rectangle and overriding both dimensions breaks code that sets width and height independently, because the invariant of the base type no longer holds. The classic C# symptom is a derived override that throws NotSupportedException for a base operation. Prefer composition or a shared abstraction that does not promise the broken behaviour.
public class Rectangle { public virtual int W { get; set; } public virtual int H { get; set; } }
public class Square : Rectangle {
public override int W { set => base.W = base.H = value; } // breaks callers
}
363. How does the dependency inversion principle relate to DI containers?
DIP says high-level policy should depend on abstractions, not on concrete infrastructure, which allows both to vary independently. A DI container is only a mechanism for supplying those abstractions at runtime and is not required by the principle. You can satisfy DIP with constructor injection and hand-built composition roots, which is often clearer for small applications.
364. Explain singleton, scoped and transient lifetimes and their risks.
Singleton is created once per root container and is shared by everything, so it must be thread-safe and must not hold request state. Scoped is created once per request or scope, which is right for DbContext and per-request caches. Transient is created per resolution, which is cheap for stateless services but can surprise you when a transient is injected into several consumers.
365. What is a captive dependency and how does it happen?
A captive dependency occurs when a longer-lived service (singleton) holds a shorter-lived one (scoped or transient), so the short-lived instance is effectively promoted and never released per scope. A typical case is a singleton capturing a scoped DbContext, which then throws or shares state across requests. The fix is to inject IServiceScopeFactory or a scoped-only refactoring, never to widen the DbContext's lifetime.
public sealed class Cache(IServiceScopeFactory factory) : IHostedService {
using var scope = factory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDb>();
}
366. How do you detect captive dependencies and scope misuse?
Build the host with ValidateScopes and ValidateOnBuild enabled in non-production environments, which makes the container throw when a singleton resolves a scoped service or a scope is used after disposal. In ASP.NET Core, the Development environment enables scope validation by default. Additionally, register DbContext as scoped and let integration tests exercise real request paths.
Host.CreateDefaultBuilder()
.UseDefaultServiceProvider(o => {
o.ValidateScopes = true;
o.ValidateOnBuild = true;
});
367. What is the difference between Add, TryAdd and AddEnumerable?
Add always appends another registration, so the last one wins for a single resolve. TryAdd registers only if no service of that type is present, which lets a library provide a default that the application can override. TryAddEnumerable registers only if no descriptor with the same implementation type exists, preventing duplicate handlers when a module is scanned twice.
368. When would you use keyed services instead of separate interfaces?
Keyed services, added in .NET 8, let one abstraction have multiple registrations distinguished by a key, which suits payment providers, storage backends, or per-tenant implementations. Resolve with GetRequiredKeyedService or the [FromKeyedServices] attribute. Separate marker interfaces remain clearer when the variants have genuinely different contracts.
builder.Services.AddKeyedSingleton<IPaymentGateway, StripeGateway>("stripe");
app.MapPost("/pay", ([FromKeyedServices("stripe")] IPaymentGateway gw) => gw.Pay());
369. How do you implement the factory pattern idiomatically with DI?
Register a factory delegate or a small factory class that resolves from IServiceProvider at call time, so the created object can have its own dependencies resolved by the container. Avoid new-ing services that have dependencies, because you lose decoration and lifetime management. Prefer a typed factory interface over injecting IServiceProvider everywhere.
370. How does the strategy pattern map onto DI in ASP.NET Core?
Define the strategy interface, register several implementations, and inject IEnumerable<IStrategy> into a resolver that selects one by a key or predicate. This keeps selection logic in one place and makes adding a strategy a matter of adding a class and a registration. It removes switch statements from the consumer.
public sealed class Router(IEnumerable<IExporter> exporters) {
public IExporter For(string format) =>
exporters.First(e => e.Format == format);
}
371. How do you add cross-cutting behaviour without touching every class?
Use the decorator pattern: implement the same interface, wrap the inner implementation, and add logging, caching or validation around the call. Registration order determines nesting, and libraries such as Scrutor can apply decorators automatically. This is preferable to inheritance and keeps the original class open for extension but closed for modification.
public sealed class CachedCatalog(ICatalog inner, IMemoryCache cache) : ICatalog {
public Task<Item?> GetAsync(int id, CancellationToken ct) =>
cache.GetOrCreateAsync($"cat:{id}", e => {
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return inner.GetAsync(id, ct);
});
}
372. When is the adapter pattern the right choice in C#?
Use it to wrap a third-party or legacy API behind an interface your domain owns, so the rest of the code depends on your abstraction rather than on a vendor's shape. The adapter translates types, exceptions and semantics, which also gives you a single place to add retries or logging. It is the standard remedy for a leaky external SDK in the application core.
public sealed class StripeAdapter(StripeClient api) : IPaymentGateway {
public async Task<Receipt> ChargeAsync(Money amount, CancellationToken ct) {
var r = await api.Charges.CreateAsync(amount.Cents, ct);
return new Receipt(r.Id, r.Status == "succeeded");
}
}
373. Explain the difference between IOptions, IOptionsSnapshot and IOptionsMonitor.
IOptions is a singleton that reads configuration once, so it never sees later changes. IOptionsSnapshot is scoped and re-reads per request, which is right for request-scoped code but must not be injected into singletons. IOptionsMonitor is a singleton that always returns the current value through .CurrentValue and raises OnChange, which is what long-lived services should use.
374. Why does injecting IOptionsSnapshot into a singleton fail?
IOptionsSnapshot is registered as scoped, so resolving it from a singleton creates a captive dependency that either throws under scope validation or silently keeps a stale instance. Use IOptionsMonitor, which is designed for singleton consumers, or inject IOptions if the value never changes. This is one of the most common DI lifetime mistakes.
375. How do named options work and when do you need them?
Named options let several configurations of the same type coexist, for example two storage accounts or three external APIs, selected by name. You register them with Configure(name, ...) and resolve with IOptionsMonitor.Get(name). They avoid inventing a wrapper class per variant.
services.Configure<ApiOptions>("billing", cfg.GetSection("Billing"));
services.Configure<ApiOptions>("shipping", cfg.GetSection("Shipping"));
var billing = provider.GetRequiredService<IOptionsMonitor<ApiOptions>>().Get("billing");
376. How do you validate options at startup rather than at first use?
Chain ValidateDataAnnotations and Validate with a predicate when registering, then call ValidateOnStart so a bad configuration fails the host immediately instead of throwing on the first request. This turns a runtime surprise into a deployment failure, which is far cheaper to diagnose. Validation runs for every named instance.
services.AddOptions<SmtpOptions>()
.Bind(cfg.GetSection("Smtp"))
.ValidateDataAnnotations()
.Validate(o => o.Port > 0, "Port must be positive")
.ValidateOnStart();
377. How does configuration precedence work across providers?
Providers are consulted in reverse registration order, so the last one added wins for a duplicated key. The default host adds appsettings.json, then appsettings.{Environment}.json, then environment variables, then command line, which is why environment variables override files. Understanding this order explains most "why is my setting ignored" incidents.
378. How do you bind configuration to a strongly typed object, and when does it reload?
Call Bind or GetSection().Get<T>() once at composition time, or use the options pattern so lifetimes and change tracking are handled for you. Binding a plain object does not track later changes; only IOptionsMonitor or an explicit reload token gives you reload semantics. Weakly typed IConfiguration access scattered through the code is a maintainability smell.
379. How should secrets be handled across environments?
Never commit secrets; use user secrets for local development, environment variables or a managed vault for deployed environments, and a managed identity to avoid stored credentials entirely. Configuration keys are read the same way regardless of the provider, so the code does not change. Rotate secrets and keep them out of logs and exception messages.
380. Why use ILogger instead of Console.WriteLine?
ILogger gives structured, categorised, levelled output that can be filtered and routed per provider without changing code, and it flows ambient context such as scopes and trace identifiers. Console output is unstructured, unfilterable, and untestable. Logging abstraction also lets tests substitute a fake logger to assert on behaviour.
381. What are logging scopes and what do they add?
A scope attaches ambient key-value data to every log entry written inside it, such as a request id, tenant or order id, without repeating those values in each call. Providers that support scopes include them in the output, which is what makes correlated logs possible. Scopes rely on AsyncLocal, so they flow across awaits but not to manually created threads.
using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = id })) {
_logger.LogInformation("Processing order");
}
382. How do log levels and categories interact with filtering?
Each log entry has a level and a category, normally the full type name, and rules can set a minimum level per category or provider. This lets you keep Information globally while raising a noisy framework namespace to Warning. Configure rules in appsettings under Logging:LogLevel, remembering that the most specific rule wins.
383. Why prefer LoggerMessage or the source-generated logging API over string interpolation?
Interpolated strings allocate and format even when the level is disabled, whereas LoggerMessage skips all work when the level is off and caches the template parsing. The source generator produces strongly typed, allocation-light methods from a partial class. In hot paths this difference is measurable.
[LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} processed in {Ms} ms")]
private static partial void LogProcessed(ILogger logger, int orderId, long ms);
384. What is the difference between IHostedService and BackgroundService?
IHostedService is the minimal contract with StartAsync and StopAsync, giving full control over lifetime. BackgroundService is an abstract base that runs ExecuteAsync on a background task and wires StopAsync to a cancellation token, which covers most long-running loops. Implement IHostedService directly when you need custom start/stop semantics or a short one-shot startup task.
385. What happens if ExecuteAsync throws in a BackgroundService?
The exception faults the background task and, depending on the host configuration and .NET version, stops the host, so an unhandled exception in a loop silently kills your worker. Always catch inside the loop, log, and decide whether to continue or stop. .NET 6+ also supports BackgroundServiceExceptionBehavior to configure this explicitly.
protected override async Task ExecuteAsync(CancellationToken ct) {
while (!ct.IsCancellationRequested) {
try { await WorkAsync(ct); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
catch (Exception ex) { _log.LogError(ex, "Worker iteration failed"); }
await Task.Delay(TimeSpan.FromSeconds(10), ct);
}
}
386. How does a BackgroundService shut down gracefully?
The host cancels the token passed to ExecuteAsync and waits for StopAsync to complete within the shutdown timeout, so honour the token in every await and exit the loop promptly. Register cleanup in finally blocks or IAsyncDisposable, and avoid blocking the token in a way that ignores cancellation. Long-running items should be checkpointed so a restart does not lose or repeat work.
387. How does a BackgroundService use scoped services?
It receives IServiceScopeFactory and creates a scope per unit of work, resolving scoped services like DbContext inside that scope, because the service itself is a singleton. Reusing one scope for the whole process lifetime effectively makes the DbContext a singleton and breaks change tracking. Keep scopes short and dispose them with using.
388. How do you queue work for background processing without losing items?
Expose a singleton queue backed by a bounded Channel, enqueue from request handlers, and have a BackgroundService consume it with await foreach. Bounded capacity gives backpressure, and completion signals drain. This pattern is fine for single-node workloads but is not durable, so use a real broker when items must survive a crash.
public sealed class Queue {
private readonly Channel<int> _ch = Channel.CreateBounded<int>(100);
public ValueTask EnqueueAsync(int id, CancellationToken ct) => _ch.Writer.WriteAsync(id, ct);
public IAsyncEnumerable<int> ReadAllAsync(CancellationToken ct) => _ch.Reader.ReadAllAsync(ct);
}
389. Why does creating a new HttpClient per request cause problems?
HttpClient is disposable and holds an HttpMessageHandler with connection pools; disposing it per request forces new TCP and TLS handshakes, causing socket exhaustion under load (TIME_WAIT accumulation). A single static client fixes that but prevents DNS refresh and centralised configuration. IHttpClientFactory solves both by pooling handlers and rotating them.
390. What does IHttpClientFactory actually manage?
It pools the underlying HttpMessageHandler, recycling each after HandlerLifetime (two minutes by default) so DNS changes are picked up, while handing out a cheap new HttpClient per resolve that can be disposed safely. It also centralises base address, default headers and resilience policies per named client. Disposing the client returned by the factory does not dispose the pooled handler.
services.AddHttpClient<IWeatherApi, WeatherApi>(c => {
c.BaseAddress = new Uri("https://api.example.com/");
c.Timeout = TimeSpan.FromSeconds(5);
});
391. Compare typed clients, named clients and raw IHttpClientFactory usage.
A typed client is a class that takes HttpClient in its constructor, giving compile-time discoverability and a natural place for endpoint methods; it is the best default. A named client resolves by string and is convenient when configuration is dynamic or you only need a configured client inline. Injecting IHttpClientFactory directly should be reserved for cases where the client must be created lazily per call.
392. Where should resilience policies be attached?
Attach them to the HttpClient pipeline via AddResilienceHandler or AddPolicyHandler so every call through that client gets consistent behaviour, rather than sprinkling try/catch around call sites. Keep retry counts low, use exponential backoff with jitter, and only retry idempotent operations. Policies should be configured per dependency because each upstream has different failure characteristics.
393. What is dangerous about blindly retrying an HTTP call?
Retrying a non-idempotent POST can duplicate a payment or an order, and retrying a slow dependency amplifies load during an outage. Retry only on transient conditions (timeouts, 5xx, 429) with bounded attempts and jitter, and require idempotency keys for writes. A retry policy is not a substitute for a timeout.
394. Explain the states of a circuit breaker.
Closed lets calls through and counts failures; when the failure threshold is crossed it trips to Open and fails fast for a cool-down period. Then it moves to Half-Open, allowing a limited number of probe calls, and closes again on success or reopens on failure. The point is to stop hammering a failing dependency so it can recover and to fail fast for callers.
395. How does a Polly timeout policy differ from HttpClient.Timeout?
HttpClient.Timeout is a hard limit for the whole request including content reading and produces a TaskCanceledException that is hard to distinguish from caller cancellation. A resilience timeout can be per attempt, composes with retries, and lets you distinguish timeout from cancellation. The usual setup is a short per-attempt timeout plus an overall budget.
396. How do you compose retry, timeout and circuit breaker in Polly v8?
Register them in one resilience pipeline in a deliberate order: the outermost policy sees the operation first, so timeout usually sits outside retry per attempt, and the circuit breaker wraps the inner calls. Polly v8 uses ResiliencePipelineBuilder with AddRetry, AddTimeout and AddCircuitBreaker, replacing the older policy-wrap syntax. Order matters far more than the individual settings.
services.AddResiliencePipeline("http", b => b
.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3, Delay = TimeSpan.FromMilliseconds(200), UseJitter = true })
.AddCircuitBreaker(new CircuitBreakerStrategyOptions { FailureRatio = 0.5, BreakDuration = TimeSpan.FromSeconds(30) })
.AddTimeout(TimeSpan.FromSeconds(5)));
397. What problem do the bulkhead and hedging strategies address?
A bulkhead caps concurrent calls to one dependency so a slow upstream cannot consume all resources and take down unrelated features. Hedging sends a duplicate request after a delay and takes the first success, reducing tail latency at the cost of extra load. Both are advanced tools that should follow measurement, not be added by default.
398. How do you choose between System.Text.Json and Newtonsoft.Json?
System.Text.Json is the default in modern .NET, is faster and allocation-light, and is what ASP.NET Core uses, so prefer it for new code. Newtonsoft.Json still wins for legacy behaviours, very flexible converters, or libraries that depend on it. Mixing both in one request pipeline creates confusing, inconsistent output.
399. When is System.Text.Json source generation useful?
Source generation moves serialisation metadata to compile time, which removes reflection and start-up cost, and it is required for trimming and Native AOT scenarios. It also surfaces unsupported constructs at build time rather than at runtime. Use JsonSerializerContext and pass its type info to the serialiser.
[JsonSerializable(typeof(Order))]
internal partial class AppJson : JsonSerializerContext { }
var json = JsonSerializer.Serialize(order, AppJson.Default.Order);
400. How do you serialise a polymorphic hierarchy with System.Text.Json?
Since .NET 7 you can annotate the base type with JsonDerivedType attributes or a type discriminator, which writes a discriminator property and restores the concrete type on deserialisation. Without it, deserialising into the base type yields only the base members. The discriminator must be stable, because it becomes part of your wire contract.
[JsonDerivedType(typeof(CardPayment), "card")]
[JsonDerivedType(typeof(BankPayment), "bank")]
public abstract record Payment(decimal Amount);
401. Where do you configure JSON options for ASP.NET Core?
Use AddControllers().AddJsonOptions for MVC and ConfigureHttpJsonOptions for minimal APIs; they are separate option objects, which is a frequent source of "my setting has no effect". Property naming policy, converters and reference handling should be set once at the composition root. Note that minimal APIs reuse the same options for both requests and responses.
402. What are the trade-offs of using AutoMapper?
It removes repetitive mapping code and centralises conventions, which pays off with many similar DTOs. The costs are indirection that is hard to debug, runtime configuration errors that surface late, and mappings that silently pick the wrong member when names drift. ProjectTo does give efficient SQL projections, but manual mapping or a compile-time generator is often clearer and faster.
403. What is the case for manual or source-generated mapping instead of reflection-based mapping?
Hand-written mappings are explicit, debuggable, and fail at compile time when a property is renamed, and source generators such as Mapperly give the same safety with less typing. They also avoid reflection cost and work under trimming and AOT. The downside is more code, which is usually cheaper than a hidden mapping bug in production.
404. How do DataAnnotations validation and FluentValidation differ?
DataAnnotations are attribute-based, declared on the model, and integrate automatically with MVC model validation, which suits simple rules. FluentValidation keeps rules in a separate class, supports conditional and cross-property rules, dependency injection in validators, and testable rule units. Choose FluentValidation once rules involve conditionals, collections or external checks.
405. Where should validation live in a layered application?
Input-shape validation belongs at the boundary, so malformed requests are rejected cheaply, while business invariants belong in the domain so they hold regardless of entry point. Duplicating a rule in both places is acceptable only if the domain remains authoritative. Relying solely on UI or controller validation lets invalid state in through other callers.
406. How should validation failures be returned from an API?
Return 400 with a ProblemDetails payload, or ValidationProblemDetails with an errors dictionary keyed by field, so clients can map messages to inputs. Keep the shape consistent across the API and avoid leaking internal exception text. ASP.NET Core produces this automatically when ModelState is invalid with [ApiController].
407. What are the limits of IMemoryCache?
It is per-process, so it is not shared across instances and cannot be invalidated cluster-wide, which matters for horizontally scaled apps. It also stores references, so mutable cached objects can be mutated by consumers. Set size limits and expirations, and treat cached values as immutable snapshots.
408. What is a cache stampede and how do you prevent it with IMemoryCache?
When a popular entry expires, many concurrent requests miss simultaneously and all hit the database, which can overload it. GetOrCreateAsync does not deduplicate the factory across concurrent callers by default. Serialise the refresh with a per-key SemaphoreSlim or a Lazy<Task<T>> entry, and consider staggered or sliding expiration.
private readonly SemaphoreSlim _gate = new(1, 1);
public async Task<Report> GetAsync(int id, CancellationToken ct) {
if (_cache.TryGetValue(id, out Report? cached)) return cached!;
await _gate.WaitAsync(ct);
try {
if (_cache.TryGetValue(id, out cached)) return cached!;
var fresh = await _repo.LoadAsync(id, ct);
_cache.Set(id, fresh, TimeSpan.FromMinutes(10));
return fresh;
} finally { _gate.Release(); }
}
409. How do you design cache keys and invalidation?
Derive keys from a stable, versioned template that includes every input that changes the result, such as tenant and entity id, so a schema change can be handled by bumping the version prefix. Prefer short TTLs plus explicit eviction on writes over complex dependency graphs. Document who invalidates what, because unowned invalidation logic is where stale data bugs live.
410. What does IDistributedCache give you and what does it not?
It abstracts a shared cache such as Redis or SQL Server so all instances see the same entries, using byte[] or string values. It is a low-level primitive with no tagging, no atomic operations and no stampede protection, so you build those or use a richer client. Serialisation is your responsibility, along with key design.
411. When is a hybrid cache appropriate?
Hybrid caching combines a local in-memory layer with a distributed backplane so hot reads avoid network hops while the distributed layer keeps instances coherent. .NET 9's HybridCache adds stampede protection, tagging and configurable serialisation on top of that idea. It is worth the complexity when read latency and database load dominate.
412. How do you decide what to abstract for testability?
Abstract the things that are slow, nondeterministic or external, such as clocks, I/O, message buses and third-party APIs, and keep pure logic concrete. Injecting an interface for every class produces ceremony without test value, since a class with no dependencies can be tested directly. Test through the smallest seam that makes the behaviour deterministic.
413. How do you make time-dependent code testable?
Inject TimeProvider (or an equivalent clock abstraction) and call GetUtcNow and CreateTimer instead of DateTime.UtcNow and new Timer. Tests can then use FakeTimeProvider to advance time deterministically without sleeping. This removes the flaky delay-based tests that plague retry and expiry logic.
public sealed class TokenService(TimeProvider clock) {
public bool IsExpired(Token t) => t.ExpiresAt < clock.GetUtcNow();
}
414. How do you test async code without flakiness?
Await the operations instead of using sleeps, use deterministic fakes for time and I/O, and control concurrency with TaskCompletionSource so you can assert intermediate states. Use timeouts as a safety net that fails the test rather than as a synchronisation mechanism. Flaky async tests usually indicate a real race or a missing await in the code under test.
415. How does WebApplicationFactory support integration testing?
It boots the real application in memory with TestServer, so middleware, routing, DI and serialisation are exercised without network calls. You can replace registrations with fakes via ConfigureTestServices, and the returned HttpClient drives real requests. This catches wiring and configuration bugs that unit tests cannot see.
public class ApiTests(WebApplicationFactory<Program> f) : IClassFixture<WebApplicationFactory<Program>> {
[Fact]
public async Task Health_returns_ok() {
var res = await f.CreateClient().GetAsync("/health");
res.EnsureSuccessStatusCode();
}
}
416. Why is lock(this) or locking on a string considered bad practice?
Locking on this or a Type exposes the lock object to external code, which can take the same lock and deadlock you or block unrelated callers. Locking on a string literal is worse because literals are interned and shared across the whole process. Use a private readonly object dedicated to the lock, or a private readonly Lock field in .NET 9+.
private readonly object _gate = new(); // not this, not typeof(X), not "lock"
417. When is ReaderWriterLockSlim better than lock?
It pays off when reads vastly outnumber writes and the critical section is long enough to matter, since concurrent readers proceed in parallel. For short or mixed workloads its overhead exceeds a plain Monitor and it is slower. It is also not async-compatible, and it is not reentrant by default.
418. How do you throttle concurrency to a fixed number of parallel operations?
Use Parallel.ForEachAsync with MaxDegreeOfParallelism, or a SemaphoreSlim acquired around each operation when you need finer control over scheduling or dynamic limits. Avoid unbounded Task.WhenAll over thousands of items, which floods the pool and downstream services. Always release the semaphore in finally so a fault does not leak permits.
var gate = new SemaphoreSlim(8);
var tasks = ids.Select(async id => {
await gate.WaitAsync(ct);
try { await _api.SendAsync(id, ct); }
finally { gate.Release(); }
});
await Task.WhenAll(tasks);
419. How do you avoid deadlocks when code needs more than one lock?
Establish a global ordering of lock objects and always acquire them in that order, so a cycle cannot form. Keep critical sections short and free of calls into unknown code that might take other locks. Where possible eliminate nested locks entirely by copying data out, releasing, then acting.
420. When would you use ConcurrentQueue instead of a Channel?
ConcurrentQueue is a lock-free collection for short, synchronous handoffs, for example accumulating completed results that are drained later. It has no async waiting and no built-in backpressure, so a consumer must poll or use its own signalling. When you need awaitable reads and a bounded capacity, a Channel is the better abstraction.
421. When are immutable collections the right choice?
They suit state shared across threads without locks, especially when readers must see a consistent snapshot and updates are relatively rare. Writes copy the structure and are more expensive than mutable adds, but reads are lock-free. They pair well with a volatile field or Interlocked swap holding the current version.
422. What are the Lazy<T> thread-safety modes?
ExecutionAndPublication is the default and guarantees the factory runs once with locking, which is nearly always what you want. PublicationOnly may run the factory several times and keep the first published value, used when the factory is safe to duplicate. None is unsynchronised and only appropriate for single-threaded access.
423. Is double-checked locking safe in C#?
Yes, if the shared field is volatile or access is done via Interlocked/Volatile.Read, because the write must publish the fully constructed object before the reference becomes visible. Without that, another thread can observe a non-null reference to a partially constructed object. In modern C#, Lazy<T> expresses the same intent more safely.
private volatile Config? _config;
public Config Current => _config ??= LoadConfig(); // safe because the field is volatile
424. What is SpinWait for and when is spinning appropriate?
SpinWait burns CPU cycles in a tight loop waiting for a very short condition, avoiding the context switch cost of blocking. It only makes sense when the wait is expected to be shorter than a context switch and threads are not oversubscribed. Long or unpredictable waits must block or await, since spinning wastes cores and can starve other work.
425. Why can't a constructor be async, and what do you do instead?
async requires a return type of void or a task-like type, and a constructor has neither, so the object would be handed out before its asynchronous initialisation completes. Use an async factory method or a static CreateAsync, or move the initialisation into an explicit InitializeAsync called by the composition root. Lazy async initialisation inside members is another option.
public sealed class Client {
private Client() { }
public static async Task<Client> CreateAsync(CancellationToken ct) {
var c = new Client();
await c.ConnectAsync(ct);
return c;
}
}
426. When do you need IAsyncDisposable and await using?
When disposal itself performs asynchronous work, such as flushing a stream or closing a connection, so a synchronous Dispose would have to block. await using calls DisposeAsync, which is often used with IAsyncEnumerable and channels. Types that own both sync and async resources should implement both interfaces.
await using var conn = new DbConnection(cs);
await conn.OpenAsync(ct);
427. When is an exception filter better than a catch-and-rethrow?
An exception filter (catch ... when (...)) inspects the exception before unwinding the stack, so if the filter returns false the stack trace is untouched and the cost is lower. It also keeps the handler logic declarative and avoids catching exceptions you cannot handle. Filters must be side-effect free and cheap.
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
return null;
}
428. How do you rethrow an exception without destroying its stack trace?
Use a bare throw inside the catch block, which preserves the original trace, rather than throw ex which resets it to the current line. To rethrow later or outside the catch, capture with ExceptionDispatchInfo.Capture and call Throw(). This matters because the original frames are usually the only useful diagnostic information.
catch (Exception ex) { _log.LogError(ex, "failed"); throw; }
429. Why should you call Flatten on an AggregateException?
Nested awaits and Task.WhenAll can wrap inner AggregateExceptions, producing deeply nested trees that are awkward to inspect. Flatten collapses them into a single level so InnerExceptions lists the real failures. This is a common need when reporting all errors from a batch operation.
430. Should a library method wrap synchronous work in Task.Run?
No. A library should expose async only where it is genuinely async, and let the caller decide whether to offload CPU-bound work, because Task.Run steals a pool thread and hides the cost. Wrapping blocking I/O in Task.Run is especially harmful since it fakes asynchrony while still blocking a thread. Document the true nature of the method instead.
431. How does a correlation id flow across async calls and service boundaries?
Inside a process, store it in AsyncLocal or rely on Activity/ILogger scopes, which flow with the execution context. Across HTTP boundaries it must be put in a header explicitly and re-established on the receiving side, usually through middleware or an HttpClient delegating handler. Baggage in OpenTelemetry automates propagation when tracing is configured.
432. How do you register an open generic service in DI?
Register the open generic type against the open generic interface, and the container closes it on demand for each type argument. This is how IRepository<T> or IValidator<T> patterns are wired without registering every closed type. The implementation must satisfy the constraints on the type parameter.
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
433. In what order does IEnumerable<T> resolution return registrations?
In registration order, which makes the order of Add calls semantically meaningful for pipelines such as middleware handlers or validators. This is convenient but fragile if registrations come from several modules, so document any order dependence. Many frameworks provide an explicit Order property precisely because relying on registration order is brittle.
434. When should you inject IServiceScopeFactory instead of IServiceProvider?
Inject IServiceScopeFactory when a long-lived service needs to create and dispose a scope for short-lived work, because it makes the scope ownership explicit and hard to forget. IServiceProvider alone allows Resolve without a scope, which silently captures scoped services. Both are service-locator flavoured, so keep their use at infrastructure edges.
435. Who is responsible for disposing services the container created?
The container owns and disposes anything it instantiated, at the end of the scope or when the root is disposed, so you must not dispose injected dependencies yourself. Disposing an injected DbContext or HttpClient breaks other consumers of the same instance. Only dispose objects you created, or register a factory and let the container track them.
436. Why is the service locator pattern discouraged?
Resolving dependencies from a container inside a class hides its requirements, so the constructor no longer documents what the type needs and tests must configure a container. It also hides lifetime bugs, because any scope can be reached. Constructor injection keeps dependencies explicit and failures discoverable at composition time.
437. How do you apply decorators to many registrations at once?
Scan the assembly with Scrutor or manual reflection, then use Decorate to wrap each matching registration while preserving its lifetime and order. This avoids writing a decorator registration per service and keeps cross-cutting concerns in one place. Be careful that the decorator is not itself scanned as a target, which produces infinite nesting.
services.Scan(s => s.FromAssemblyOf<IOrderService>()
.AddClasses(c => c.AssignableTo<IOrderService>())
.AsImplementedInterfaces().WithScopedLifetime());
services.Decorate<IOrderService, LoggingOrderService>();
438. How does the open/closed principle show up in C# without over-engineering?
New behaviour should arrive as a new implementation rather than an edit to a switch inside an existing class, so the extension point is an interface resolved through DI. That is the practical version of OCP; it does not mean never editing a file. Applying it everywhere produces needless abstraction, so reserve it for areas known to vary.
439. What does interface segregation look like in a real codebase?
Split a fat service interface into role interfaces such as IOrderReader and IOrderWriter so consumers depend only on what they use, which also makes fakes trivial. The smell is an interface where every test fake must throw NotImplementedException for half the members. Smaller interfaces also improve the ability to decorate selectively.
440. Compare anemic and rich domain models.
An anemic model is a bag of properties with logic living in services, which is simple and fine for CRUD-heavy applications but lets invariants be bypassed. A rich model puts behaviour and invariant enforcement in the entities, which suits complex domains but complicates persistence and mapping. The right choice depends on how much domain logic actually exists, not on dogma.
441. What is primitive obsession and how do records help?
Primitive obsession is representing domain concepts as string or decimal everywhere, so a customer id and an order id are interchangeable and no validation is enforced. A small value object such as a record struct with a validating factory makes the concept explicit and type-safe. The compiler then prevents mixing unrelated ids.
public readonly record struct CustomerId(Guid Value) {
public static CustomerId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString("N");
}
442. When should you return a result object instead of throwing?
Use a result type for expected, recoverable outcomes that callers must handle, such as validation failures or a not-found lookup, because exceptions are for exceptional conditions and are costly. Exceptions remain appropriate for programming errors and truly unexpected faults. Mixing both without a convention is what makes codebases confusing.
public readonly record struct Result<T>(bool Ok, T? Value, string? Error) {
public static Result<T> Success(T v) => new(true, v, null);
public static Result<T> Fail(string e) => new(false, default, e);
}
443. What makes a retried operation idempotent in practice?
An operation is idempotent when repeating it produces the same result, which you achieve with an idempotency key persisted by the server, upserts instead of inserts, or conditional writes. For HTTP this typically means a client-supplied request key with a short TTL, plus returning the original response on replay. Without it, retries duplicate side effects.
444. What problem does the outbox pattern solve?
It solves the dual-write problem: writing to the database and publishing a message are two systems, so one can succeed and the other fail. The message is stored in an outbox table within the same transaction as the state change, and a background relay publishes it with at-least-once delivery. Consumers must then be idempotent.
445. How do you keep feature flags from becoming untestable complexity?
Wrap flag evaluation behind a small interface, keep flag names and defaults in one place, and treat each combination that matters as an explicit test case. Remove flags shortly after rollout instead of letting them accumulate, because stale flags create untested branches. Inject the flag provider so tests can force each state deterministically.
446. How do you verify that a Span or ValueTask optimisation actually helped?
Measure before and after with BenchmarkDotNet, checking allocations as well as time, because these optimisations trade readability for allocation savings. A micro-benchmark in isolation can mislead if the real workload is dominated by I/O. Revert the change when the numbers do not justify the complexity.
[MemoryDiagnoser]
public class ParseBench {
[Benchmark(Baseline = true)] public int Substring() => Parse(_s.Substring(0, 4));
[Benchmark] public int Span() => Parse(_s.AsSpan(0, 4));
}
447. When is defining a custom exception type justified?
When callers need to catch it specifically and act differently, or when it carries structured data that helps handling, such as the conflicting resource id. Otherwise a built-in exception with a clear message is enough and does not expand the API surface. Custom exceptions should be serialisable, documented, and never used for control flow that happens routinely.
448. How do you keep a complex multi-step async workflow readable?
Extract each step into a named method that returns a meaningful result, so the orchestrator reads as a sequence of intentions, and pass one CancellationToken through all of them. Compensating actions belong in explicit try/catch or a saga-style coordinator rather than deep nesting. If the flow has many branches and retries, model it as a state machine instead of straight-line code.
449. How do you bound memory in a long-running consumer?
Use a bounded queue or channel so producers block when consumers fall behind, and process in batches with a fixed maximum. Avoid unbounded caches and unbounded Task.WhenAll fan-out, both of which grow with load. Monitor queue depth and memory as first-class metrics so the bound is visible before an OOM.
450. What concurrency red flags do you look for in a code review?
Blocking calls such as .Result and .Wait in async paths, async void outside event handlers, fire-and-forget tasks without error handling, and shared mutable state on singletons. I also look for cancellation tokens that are accepted but never passed on, and lock objects that are publicly reachable. Each of these is cheap to spot and expensive to debug in production.
451. What is the generic host in ASP.NET Core and how does it differ from the older WebHost?
The generic host (Host.CreateDefaultBuilder) is a general-purpose host that can run web and non-web workloads, while WebHost was web-only. The generic host exposes a single DI container, configuration, logging and lifetime management for any hosted service, with ConfigureWebHostDefaults layering Kestrel and the ASP.NET Core pipeline on top.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<Worker>();
var app = builder.Build();
app.MapGet("/", () => "ok");
app.Run();
452. What does WebApplication.CreateBuilder do for you by default?
It creates a pre-configured builder that already wires configuration (appsettings.json, environment variables, command line), logging providers, Kestrel, routing, and the default service provider. It also sets the content root and environment from environment variables such as ASPNETCORE_ENVIRONMENT. You then register your own services and call Build() to get a WebApplication that acts as both host and pipeline.
453. How do you configure Kestrel endpoints and limits in ASP.NET Core?
Endpoints and limits are configured through KestrelServerOptions, either in code via builder.WebHost.ConfigureKestrel or through the Kestrel section of configuration. You can set listen options (URLs, HTTPS certificates, HTTP protocols) and limits such as max request body size, header size and concurrent connections.
builder.WebHost.ConfigureKestrel(o =>
{
o.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
o.ListenAnyIP(5001, l => l.UseHttps());
});
454. When would you host ASP.NET Core behind IIS or a reverse proxy instead of exposing Kestrel directly?
Kestrel is fully capable of serving traffic directly, but a reverse proxy adds TLS termination, centralized certificate management, request buffering, load balancing and an extra security boundary. Behind IIS the app runs with the ASP.NET Core Module and Kestrel listens on a loopback or named pipe. In that setup you must enable forwarded headers so the app sees the original scheme and client IP.
455. What is the ASP.NET Core middleware pipeline and how is it composed?
Middleware are components registered in order on IApplicationBuilder, each receiving a RequestDelegate for the next component. Requests flow down the chain and responses flow back up, so the order of registration determines behaviour. A middleware can short-circuit by not calling next, which is how authentication or static files can end a request early.
app.Use(async (ctx, next) =>
{
var sw = Stopwatch.StartNew();
await next();
_log.LogInformation("{Path} took {Ms}ms", ctx.Request.Path, sw.ElapsedMilliseconds);
});
456. Why does middleware ordering matter, and what is a typical correct order?
Each middleware only sees requests that reach it, so anything that must run for all requests (exception handling, HSTS, forwarded headers) has to be registered before components that can short-circuit. A common order is exception handler, HSTS/HTTPS redirection, routing, CORS, authentication, authorization, rate limiting and finally endpoints. Getting it wrong produces subtle bugs such as 401 responses without CORS headers or exceptions that bypass your error handler.
457. What is the difference between UseRouting and UseEndpoints, and how did minimal hosting change it?
UseRouting matches the incoming request to an endpoint and stores it on the HttpContext; UseEndpoints executes the selected endpoint. Middleware between them can inspect the matched endpoint, which is how authorization metadata is applied. With WebApplication, MapGet and friends register endpoints and routing/endpoint middleware are added automatically at the right position.
458. How do you write a custom middleware class and register it correctly?
A conventional middleware is a class with a constructor taking RequestDelegate and an InvokeAsync(HttpContext) method, registered with UseMiddleware<T>(). Constructor dependencies are resolved once from the root provider, so scoped services must be injected into InvokeAsync instead. Alternatively you can implement IMiddleware, which is resolved per request from the request scope.
public sealed class TimingMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext ctx, ILogger<TimingMiddleware> log)
{
await next(ctx);
}
}
459. How does branching work with Map and MapWhen in the pipeline?
Map branches on a path prefix and re-joins only if the branch calls next; MapWhen branches on an arbitrary predicate such as a header or query string. Branches are terminal by default once they handle the request. These are useful for isolating health endpoints or legacy pipelines, though endpoint routing is usually preferred today.
460. How does attribute routing differ from conventional routing?
Attribute routing declares templates directly on controllers and actions, giving each action an explicit, local URL shape. Conventional routing defines global templates with placeholders such as {controller}/{action}/{id?} and relies on naming conventions. Attribute routing is the default choice for APIs because it is explicit and supports per-action constraints and versioning.
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpGet("{id:int}")]
public ActionResult<OrderDto> Get(int id) => ...;
}
461. What route constraints are available and when should you use them?
Constraints such as int, guid, bool, datetime, min, max, length, alpha and regex restrict which values match a template. They prevent ambiguous matches and reject obviously invalid URLs before model binding runs. Prefer built-in constraints over regex for readability and performance.
[HttpGet("{id:guid}")]
[HttpGet("range/{page:int:min(1)}")]
462. How do you create a custom route constraint?
Implement IRouteConstraint with a Match method, then register it with options.ConstraintMap.Add("slug", typeof(SlugConstraint)) in AddRouting. Reference it in a template as {slug:slug}. Constraints should be cheap and side-effect free because they run during route matching.
463. What are the model binding sources in ASP.NET Core and what are their defaults?
Values can come from the route, query string, form body, headers, or the request body as JSON. Simple types bind from route and query by default, complex types bind from the body, and [FromBody] allows only one parameter. Explicit attributes such as [FromRoute], [FromQuery], [FromHeader] and [FromForm] remove ambiguity.
public IActionResult Search([FromQuery] string q, [FromQuery] int page = 1) => ...;
464. Why can only one parameter bind from the body in a controller action?
The request body is a forward-only stream, so the framework can only read and deserialize it once. If you need several values, wrap them in a single DTO. Minimal APIs solved this differently by generating code that reads the body once and distributes the values.
465. How does model validation work with DataAnnotations in ASP.NET Core?
Validation attributes on the model are evaluated by the model binder and recorded in ModelState. With [ApiController] an invalid ModelState automatically produces a 400 ProblemDetails response, so controllers rarely need explicit checks. Attributes such as [Required], [Range], [StringLength] and [RegularExpression] cover most rules.
public record CreateOrderDto(
[Required, StringLength(50)] string Customer,
[Range(1, 1000)] int Quantity);
466. When would you choose FluentValidation over DataAnnotations?
FluentValidation keeps validation rules outside the DTO, supports complex conditional and cross-property rules, and is easy to unit test in isolation. It also handles async validation such as uniqueness checks against a database. DataAnnotations are lighter and zero-dependency, so they suit simple contracts.
public class CreateOrderValidator : AbstractValidator<CreateOrderDto>
{
public CreateOrderValidator()
{
RuleFor(x => x.Quantity).GreaterThan(0);
RuleFor(x => x.Customer).NotEmpty().MaximumLength(50);
}
}
467. How do you integrate FluentValidation so invalid models return 400 automatically?
Register validators with AddValidatorsFromAssemblyContaining<T>(), then add a custom IValidatorInterceptor or configure ApiBehaviorOptions.InvalidModelStateResponseFactory. With automatic validation enabled, failures are copied into ModelState and the standard ProblemDetails response is produced. This keeps controllers free of validation plumbing.
468. What is the difference between action filters, resource filters and exception filters?
Resource filters run before model binding and after the result, which makes them suitable for caching or short-circuiting expensive work. Action filters run around the action method after binding and are used for logging, validation or result transformation. Exception filters catch unhandled exceptions from the action and can replace the result without unwinding the whole pipeline.
469. How do you register a filter globally versus per controller or per action?
Global filters are added through MvcOptions.Filters; controller and action level filters are applied as attributes. Attributes are instantiated by the framework unless you use [ServiceFilter] or [TypeFilter] to resolve them from DI. Global filters apply to every action, so they must be cheap and safe.
builder.Services.AddControllers(o =>
o.Filters.Add<GlobalExceptionFilter>());
470. What is ProblemDetails and which RFC defines it?
ProblemDetails is a machine-readable error payload defined by RFC 7807 and clarified by RFC 9457, with members type, title, status, detail and instance. ASP.NET Core can produce it automatically for validation failures and status codes via AddProblemDetails(). Clients get one consistent error shape regardless of where the failure occurred.
{
"type": "https://example.com/errors/out-of-stock",
"title": "Out of stock",
"status": 409,
"detail": "SKU 123 has 0 units available"
}
471. How do you customize the automatic 400 response for validation errors?
Configure ApiBehaviorOptions.InvalidModelStateResponseFactory to build the body you want, typically a ValidationProblemDetails with an errors dictionary. You can add a correlation id or change the media type there. Returning a consistent shape helps clients map field errors to forms.
o.InvalidModelStateResponseFactory = ctx =>
new BadRequestObjectResult(new ValidationProblemDetails(ctx.ModelState));
472. How do you choose the correct HTTP status code for a POST that creates a resource?
Return 201 Created with a Location header pointing at the new resource, usually through CreatedAtAction or Created. If the resource already exists and the call is effectively idempotent, 200 with the existing representation is often better. Use 202 Accepted when creation is asynchronous.
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
473. Which status codes should a well-designed API use for common failures?
400 for malformed or invalid input, 401 when credentials are missing or invalid, 403 when authenticated but not permitted, 404 for missing resources, 409 for state conflicts, 422 for semantic validation errors and 412 for failed preconditions. 500 means an unexpected server fault, and 503 signals temporary unavailability. Being consistent matters more than being exhaustive.
474. When is 401 versus 403 the right response?
401 means the request lacks valid authentication, so the client should authenticate and retry. 403 means the identity is known but the action is not allowed. Returning 403 for unauthenticated users hides the need to log in, and returning 401 for authorized-but-forbidden users causes pointless retry loops.
475. How should REST resources be named and modelled?
Use plural nouns for collections such as /orders and /orders/{id}/items for sub-resources, avoiding verbs in paths. Keep resource identity in the URL and let HTTP methods express the operation. Represent relationships by nesting only when the child cannot exist independently.
476. Which HTTP methods are safe and which are idempotent?
GET, HEAD and OPTIONS are safe because they do not change server state. GET, HEAD, PUT, DELETE and OPTIONS are idempotent, so repeating them yields the same state. POST and PATCH are generally neither safe nor idempotent, which is why retries need care.
477. What is the difference between PUT, PATCH and POST?
POST creates a subordinate resource or triggers processing and is neither safe nor idempotent. PUT replaces the full representation at a known URI and is idempotent. PATCH applies a partial modification and is only idempotent if the patch document is designed that way.
PUT /orders/42 { "status": "shipped", "items": [...] }
PATCH /orders/42 { "op": "replace", "path": "/status", "value": "shipped" }
478. How would you design pagination for a large collection endpoint?
Offer page size and page number or an opaque cursor, cap the maximum page size, and return pagination metadata either in the body envelope or via Link headers. Offset pagination is simple but degrades and drifts on large or changing datasets; keyset or cursor pagination is stable and faster. Document default and maximum sizes.
{
"items": [ ... ],
"page": 2,
"pageSize": 25,
"totalCount": 1043,
"next": "/orders?page=3&pageSize=25"
}
479. What is a keyset or cursor pagination scheme and why use it?
Instead of an offset, the client sends the last seen sort key, for example an encoded (createdAt, id) tuple. The query then filters with WHERE (createdAt, id) > (@c, @i) and uses an index efficiently. This avoids deep-offset scans and does not skip or duplicate rows when data changes between pages.
480. How do you implement filtering and sorting without creating security or performance problems?
Expose an explicit allow-list of filterable and sortable fields rather than accepting arbitrary property names, and build the query dynamically against that list. Always parameterize values and cap the number of combined filters. Whitelisting prevents expression injection and accidental full-table scans.
GET /orders?status=open&sort=-createdAt&createdAfter=2026-01-01
481. What are sparse fieldsets and how do you implement them?
Sparse fieldsets let the client request only the fields it needs, for example ?fields=id,total,status. The server validates the requested names against a whitelist and projects the response, often with a dictionary or a dynamic DTO. This reduces payload size but complicates caching and typing.
482. Which API versioning strategies exist and how do they compare?
URI path versioning (/v1/orders) is explicit and cache friendly; query string versioning is easy but pollutes URLs; header or media type versioning keeps URLs clean but is harder to test and cache. Choose one primary strategy and document deprecation policy. The Asp.Versioning.Mvc package supports all of them plus api-supported-versions headers.
builder.Services.AddApiVersioning(o =>
{
o.DefaultApiVersion = new ApiVersion(1, 0);
o.AssumeDefaultVersionWhenUnspecified = true;
o.ReportApiVersions = true;
});
483. How does content negotiation work in ASP.NET Core?
The framework inspects the Accept header and picks the first output formatter that can write the requested media type, falling back to a 406 if none matches. Formatters are configured in MvcOptions.OutputFormatters and their order determines priority. JSON is the default, with System.Text.Json as the serializer.
484. How do you add a custom output formatter such as CSV?
Derive from TextOutputFormatter, declare supported media types and encodings, and write to the response body in WriteResponseBodyAsync. Register it in OutputFormatters before or after the JSON formatter depending on desired priority. Restrict it with [Produces("text/csv")] where appropriate.
public sealed class CsvFormatter : TextOutputFormatter
{
public CsvFormatter()
{
SupportedMediaTypes.Add("text/csv");
SupportedEncodings.Add(Encoding.UTF8);
}
}
485. How do you generate OpenAPI documents in modern ASP.NET Core?
AddOpenApi() and MapOpenApi() build the document at runtime from endpoint metadata, replacing Swashbuckle for many scenarios. UI tooling can be layered on top, or the JSON document can feed client generators and contract tests. Document only endpoints you want published.
builder.Services.AddOpenApi();
app.MapOpenApi();
app.MapGet("/orders/{id:int}", (int id) => ...).WithSummary("Get an order");
486. How do you document response types and examples for OpenAPI?
Use [ProducesResponseType] attributes or Produces<T>() on minimal APIs so status codes and schemas appear in the document. Add XML comments by enabling GenerateDocumentationFile and wiring the XML path, and enrich schemas with examples through transformers or annotations. Accurate response metadata is what makes generated clients usable.
487. When should you prefer minimal APIs over controllers?
Minimal APIs reduce ceremony for small, focused endpoints and start faster with less reflection. Controllers still win for large APIs that lean on filters, model binding conventions, and shared base classes. Many teams mix both in one application because they share routing and DI.
488. How do endpoint filters work in minimal APIs?
An endpoint filter implements IEndpointFilter and wraps the handler invocation, so it can inspect arguments, short-circuit, or modify the result. Filters are added per endpoint, per route group, or globally via AddEndpointFilter. They are the minimal API counterpart of action filters.
app.MapPost("/orders", CreateOrder)
.AddEndpointFilter<ValidationFilter>();
489. What are route groups and what problem do they solve?
MapGroup("/api/orders") creates a shared prefix and metadata scope so conventions such as authorization, tags or filters are applied once. Groups can be nested and are not themselves endpoints. This removes repetitive prefixes and attributes across related handlers.
var orders = app.MapGroup("/api/orders").RequireAuthorization().WithTags("Orders");
orders.MapGet("/", ListOrders);
orders.MapGet("/{id:int}", GetOrder);
490. What is IResult and why do TypedResults matter?
IResult is the minimal API abstraction for producing an HTTP response. TypedResults returns concrete types such as Ok<T>, which makes handler signatures explicit and improves OpenAPI generation and unit testing. It replaces the untyped Results helpers in most new code.
static Results<Ok<OrderDto>, NotFound> Get(int id, IOrderService svc) =>
svc.Find(id) is { } o ? TypedResults.Ok(o) : TypedResults.NotFound();
491. How does parameter binding work in minimal APIs?
Simple types bind from the route by default, and other sources are declared explicitly with attributes such as [FromQuery], [FromHeader] or [FromBody]. Complex types default to the body, and well-known services like HttpContext, ClaimsPrincipal or CancellationToken are injected. The [AsParameters] attribute lets you group many query values into a single record.
492. How do you validate request bodies in minimal APIs?
Minimal APIs do not run DataAnnotations automatically before .NET 10, so teams either use an endpoint filter, a FluentValidation filter, or explicit validation in the handler. With .NET 10 you can call AddValidation() and annotate the DTO to get automatic 400 responses. Whatever the mechanism, validate at the boundary and return ProblemDetails.
app.MapPost("/orders", (CreateOrderDto dto) => ...).AddValidation();
493. What service lifetimes does ASP.NET Core DI support and how do they differ?
Singleton instances live for the application, scoped instances live for one request scope, and transient instances are created per resolution. A scoped service resolved from the root provider is effectively a singleton, which is a common bug. Keep scoped lifetimes for DbContext, request-bound state and per-request caches.
494. Why is injecting a scoped service into middleware's constructor a bug?
Middleware is constructed once from the root provider, so a constructor-injected scoped service becomes a captive dependency shared across all requests. That can leak state, share a DbContext across threads and corrupt data. Inject the scoped service into InvokeAsync or use IMiddleware.
495. How do you safely resolve scoped services inside a singleton background service?
Inject IServiceScopeFactory and create a scope per unit of work, resolving scoped services from that scope and disposing it afterwards. Never hold a scoped service in a field of the singleton. This pattern is standard for hosted services and queue consumers.
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Orders.AddAsync(order);
496. What is the difference between GetRequiredService and GetService?
GetService<T>() returns null when the service is not registered, while GetRequiredService<T>() throws InvalidOperationException. Prefer the required variant for mandatory dependencies so misconfiguration fails fast rather than with a null reference. Neither should be used for control flow in application code.
497. What are the pitfalls of using IHttpContextAccessor?
It relies on an AsyncLocal, so the context is only valid inside the request flow and may be null in background threads or after the request completes. It hides dependencies and makes code harder to test. Prefer passing the values you need explicitly, and if you must use it, only read inside the active request.
498. How does configuration work in ASP.NET Core and what is the precedence order?
Configuration is a stack of providers; later providers override earlier ones, so appsettings.json, appsettings.{Environment}.json, user secrets, environment variables and command line args apply in that increasing order. Keys are flat with : separators and bind to strongly typed options. Reload-on-change is supported for file providers.
499. What are user secrets and when should they not be used?
User secrets store development-only values outside the repository in the user profile, keeping connection strings and API keys out of source control. They are never encrypted and are not a production secret store. In production use environment variables, a vault or a managed secret service.
dotnet user-secrets init
dotnet user-secrets set "Smtp:Password" "dev-only"
500. How do you bind configuration to a strongly typed options class?
Use services.Configure<T>(configuration.GetSection("Section")) and inject IOptions<T>, IOptionsSnapshot<T> or IOptionsMonitor<T>. IOptions is a singleton, IOptionsSnapshot is scoped and recomputed per request, and IOptionsMonitor supports change notifications. Choose based on whether you need reloads.
501. How do you validate options at startup so misconfiguration fails fast?
Call AddOptions<T>().Bind(section).ValidateDataAnnotations().ValidateOnStart() so validation runs during startup rather than on first use. For complex rules use Validate with a predicate or implement IValidateOptions<T>. Failing at boot is far cheaper than failing on the first production request.
builder.Services.AddOptions<SmtpOptions>()
.Bind(builder.Configuration.GetSection("Smtp"))
.ValidateDataAnnotations()
.ValidateOnStart();
502. How do logging scopes help in ASP.NET Core?
A scope attaches key-value pairs to every log entry written within it, so correlation ids, tenant ids or order ids appear on all related messages. The framework already creates a scope per request with the request id and path. Add your own keys with ILogger.BeginScope.
using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = id }))
{
_logger.LogInformation("Processing order");
}
503. What does structured logging mean and why avoid string interpolation in log calls?
Structured logging stores message templates plus named properties rather than a flat string, so logs can be queried by field. Interpolated strings destroy that structure and allocate even when the level is disabled. Use message templates with placeholders and pass values as arguments.
_logger.LogInformation("Order {OrderId} shipped to {Country}", id, country);
504. How do health checks differ between liveness and readiness?
Liveness answers whether the process is healthy enough to keep running, so orchestrators restart it on failure. Readiness answers whether the app can serve traffic, including dependencies such as the database, so traffic is withheld rather than the pod killed. Expose them on separate endpoints with different checks and tags.
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("db", tags: new[] { "ready" });
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });
505. Why should health check endpoints be excluded from authentication and rate limiting?
Probes are called frequently by infrastructure that cannot present credentials, and blocking them causes false failures and restarts. Rate limiting can also throttle probes during traffic spikes, triggering cascading outages. Map them before auth middleware or mark them AllowAnonymous and exempt from limits.
506. How does CORS work and why is a preflight request needed?
The browser enforces same-origin policy and asks the server, via an OPTIONS preflight, whether the origin, method and headers are allowed. The server responds with Access-Control-Allow-Origin and related headers. Non-simple requests and credentialed requests always trigger preflight, and the response must name an explicit origin when credentials are used.
builder.Services.AddCors(o => o.AddPolicy("spa", p =>
p.WithOrigins("https://app.example.com").AllowAnyHeader().AllowAnyMethod().AllowCredentials()));
app.UseCors("spa");
507. What is the most common cause of CORS failures in ASP.NET Core?
Middleware order: UseCors must run after UseRouting and before authentication and authorization so headers are added even to 401 responses. Another common cause is combining AllowAnyOrigin with AllowCredentials, which the spec forbids. Always test the actual preflight response, not just the GET.
508. How does the built-in rate limiting middleware work?
AddRateLimiter registers policies such as fixed window, sliding window, token bucket and concurrency limiter, and UseRateLimiter applies them. Policies can be partitioned by IP, API key, user or route. Rejected requests return 503 by default, which you should normally change to 429 with a Retry-After header.
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
});
509. How do you partition a rate limit per API key or per user?
Use AddPolicy with a partition key selector that reads a header, claim or IP address. Hash or truncate the key so raw secrets never become dictionary keys. Also bound the number of partitions so memory cannot grow without limit.
o.AddPolicy("per-key", ctx =>
RateLimitPartition.GetFixedWindowLimiter(
ctx.Request.Headers["X-Api-Key"].ToString(),
_ => new FixedWindowRateLimiterOptions { PermitLimit = 60, Window = TimeSpan.FromMinutes(1) }));
510. How does response compression work and what should you not compress?
UseResponseCompression negotiates gzip, brotli or deflate based on Accept-Encoding and compresses responses above a size threshold. Do not compress already-compressed content such as images, video or zip files, and avoid compressing responses containing secrets over unencrypted channels. HTTPS plus compression can also expose BREACH-style attacks on reflected secrets.
511. What is the difference between response caching and output caching?
Response caching sets HTTP cache headers so clients and intermediaries can cache, but the server still executes the request. Output caching stores the rendered response server-side and can serve it without running the endpoint, with support for tags and eviction. Output caching is the better fit for expensive read endpoints you control.
builder.Services.AddOutputCache(o =>
o.AddPolicy("orders", b => b.Expire(TimeSpan.FromSeconds(30)).Tag("orders")));
app.UseOutputCache();
512. Why must you be careful caching responses for authenticated requests?
A shared cache keyed only by URL can serve one user's data to another, which is a serious data leak. Vary the cache key by user, tenant or relevant headers, or disable caching for personalized endpoints and send Cache-Control: private, no-store. Output caching ignores Authorization by default, so it must be configured explicitly.
513. How does IDistributedCache differ from IMemoryCache?
IMemoryCache is in-process, fast, and lost on restart, so it is inconsistent across instances. IDistributedCache targets Redis, SQL Server or similar and shares state across nodes, at the cost of serialization and network latency. Use the distributed cache for session-like data and coordination, and memory cache for local hot data.
514. What cache stampede problem exists and how do you mitigate it?
When a hot key expires, many concurrent requests miss and all recompute or refetch simultaneously, overloading the backend. Mitigations include per-key locking or SemaphoreSlim, stale-while-revalidate, jittered expiry and early refresh before expiry. HybridCache in .NET 9 addresses this with built-in stampede protection.
515. What does the SameSite cookie attribute do?
It controls whether a cookie is sent on cross-site requests: Strict never sends it, Lax sends it on top-level GET navigation, and None always sends it but requires Secure. The default in modern browsers is Lax. This is a primary defence against CSRF for cookie-based auth.
516. How do you configure cookie security flags in ASP.NET Core?
Set HttpOnly, Secure, SameSite, a path and a bounded lifetime on the cookie options, and consider __Host- prefixes for strict scoping. Use CookiePolicyOptions to enforce defaults globally. HttpOnly prevents JavaScript access, mitigating XSS-based token theft.
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
517. How does JWT bearer authentication work in ASP.NET Core?
AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer() configures token validation against an authority's signing keys, issuer and audience. The handler validates signature, lifetime, issuer and audience, then builds a ClaimsPrincipal from the payload. Authorization middleware then evaluates policies against those claims.
builder.Services.AddAuthentication().AddJwtBearer(o =>
{
o.Authority = "https://id.example.com";
o.Audience = "orders-api";
o.TokenValidationParameters.ValidateIssuer = true;
});
518. Where should a browser-based SPA store access tokens?
Local storage is readable by any script, so an XSS flaw leaks the token; an HttpOnly cookie is not readable but needs CSRF protection. A common compromise is a short-lived access token in memory plus a refresh token in a Secure, HttpOnly, SameSite cookie. Better still, use a backend-for-frontend that keeps tokens server-side.
519. How does cookie authentication differ from bearer token authentication?
Cookie authentication stores an encrypted ticket in a browser cookie that is sent automatically, which suits server-rendered apps but requires CSRF defence. Bearer tokens must be attached explicitly by the client and suit APIs and mobile apps. Cookies rely on the browser's ambient authority; tokens do not.
520. How do you implement policy-based authorization?
Register policies in AddAuthorization with requirements, then apply them with [Authorize(Policy = "...")] or RequireAuthorization. Each requirement is evaluated by one or more handlers deriving from AuthorizationHandler<TRequirement>. Policies compose claims, roles and custom logic in one named place.
builder.Services.AddAuthorization(o =>
o.AddPolicy("CanApprove", p => p.RequireClaim("permission", "orders.approve")));
521. What is a claims transformation and when is it useful?
An IClaimsTransformation runs after authentication and can add, remove or enrich claims on the principal, for example mapping an external id to internal roles. It is useful for reconciling identity provider claims with application permissions. Keep it fast and avoid database calls on every request without caching.
522. How does resource-based authorization work?
You call IAuthorizationService.AuthorizeAsync(User, resource, policy) from the handler after loading the resource, so the decision depends on the actual entity rather than just claims. This pattern fits "owner or admin may edit this document" rules that URL-level attributes cannot express. Return 403 or 404 consistently to avoid leaking existence.
var result = await _auth.AuthorizeAsync(User, order, "OrderOwner");
if (!result.Succeeded) return Forbid();
523. What does ASP.NET Core Identity provide out of the box?
Identity supplies user and role stores, password hashing, lockout, two-factor flows, token providers and the UserManager/SignInManager APIs, backed by EF Core by default. It is a full membership system, not just a login endpoint. For API-only scenarios many teams prefer an external identity provider such as Duende or Entra ID.
524. How does antiforgery protection work in ASP.NET Core?
The server issues a token bound to the user session and expects it back in a header or form field on state-changing requests. [ValidateAntiForgeryToken] or the automatic validation for Razor Pages rejects requests without a matching token. Cookie-authenticated APIs need this; pure bearer-token APIs generally do not because browsers do not attach tokens automatically.
525. What do HSTS and HTTPS redirection do and why both?
HTTPS redirection sends a 307 to the https scheme for that request, but the first request still travels in clear text. HSTS tells the browser to use HTTPS for the domain for a period, closing that gap, and UseHsts emits the header only outside development. Use both, and never enable HSTS on a domain you cannot serve over TLS.
app.UseHsts();
app.UseHttpsRedirection();
526. Which security headers should an API send?
At minimum X-Content-Type-Options: nosniff, Referrer-Policy and, for browser-facing responses, a restrictive Content-Security-Policy. Add X-Frame-Options or CSP frame-ancestors to prevent clickjacking, and remove headers that leak server details. APIs returning only JSON need fewer headers than HTML apps but should still avoid MIME sniffing.
527. How do you handle file uploads safely in ASP.NET Core?
Accept IFormFile or stream with MultipartReader, enforce a size limit, validate content type and extension, and never trust the client-provided filename. Store outside the web root, generate your own names, and scan for malware when accepting untrusted content. Stream to storage rather than buffering the whole file in memory.
[HttpPost("upload")]
[RequestSizeLimit(25_000_000)]
public async Task<IActionResult> Upload(IFormFile file, CancellationToken ct)
{
await using var stream = File.Create(Path.Combine(_root, Guid.NewGuid().ToString("N")));
await file.CopyToAsync(stream, ct);
return NoContent();
}
528. How do you stream a large download without loading it into memory?
Return a FileStreamResult or write directly to Response.Body with CopyToAsync, optionally with enableRangeProcessing for partial content. Set content type and length when known, and never build the payload in a byte array. This keeps memory flat regardless of file size.
return File(stream, "application/octet-stream", enableRangeProcessing: true);
529. How do you raise the request body size limit for a single endpoint?
Use [RequestSizeLimit] on a controller action or IHttpMaxRequestBodySizeFeature in minimal APIs, and keep the global Kestrel limit low. IIS and any reverse proxy also impose their own limits that must be raised separately. Raising global limits invites memory exhaustion attacks.
530. How do you handle large payloads you do not want to buffer?
Stream the request body with MultipartReader or JsonSerializer.DeserializeAsyncEnumerable, and stream responses incrementally with IAsyncEnumerable. Disable request buffering where appropriate and apply backpressure. Buffering a 100 MB JSON document is a reliable way to trigger an out-of-memory condition.
531. How do you run background work inside a web application correctly?
Implement IHostedService or derive from BackgroundService and register it with AddHostedService. The host manages start and graceful stop through the cancellation token, and the work survives beyond individual requests. Do not capture request-scoped services or fire-and-forget from a controller.
public sealed class OutboxWorker(IServiceScopeFactory scopes) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested) { /* create scope, process */ }
}
}
532. Why is firing a background task from a controller with Task.Run dangerous?
The task is unobserved and untracked, so it can be killed on app recycle, lose its DI scope and swallow exceptions. The request may also complete while the work is still writing to a disposed DbContext. Use a hosted service with a queue such as Channel<T>, or a durable message broker.
533. What is SignalR and when would you use it?
SignalR provides real-time bidirectional messaging over WebSockets with fallbacks to server-sent events and long polling. It handles connection management, groups and reconnection for you. Use it for notifications, dashboards and chat rather than trying to poll an API.
public class OrderHub : Hub
{
public Task Subscribe(string orderId) => Groups.AddToGroupAsync(Context.ConnectionId, orderId);
}
534. How does SignalR scale out across multiple servers?
Because connections are pinned to a server, you need a backplane such as Redis or Azure SignalR Service to broadcast across instances. Sticky sessions alone are insufficient for group messaging. Also consider connection limits, message size and authentication on the negotiate step.
535. When would you choose gRPC over REST?
gRPC is a strong fit for internal service-to-service calls with low latency, streaming and strict contracts over HTTP/2 with Protobuf. REST with JSON wins for public APIs, browser clients and broad tooling support. gRPC-Web or JSON transcoding bridges browser scenarios when needed.
536. What are the practical constraints of gRPC in ASP.NET Core?
It requires HTTP/2, which affects hosting behind some proxies and on older IIS setups, and browsers need gRPC-Web. Protobuf messages are not human-readable, and load balancers must support end-to-end HTTP/2 for streaming. Deadlines, cancellation and status codes are part of the contract and should be set explicitly.
537. What is YARP and why use it instead of nginx?
YARP is a reverse proxy library for ASP.NET Core, configured in code or appsettings with routes and clusters, and it participates in the same DI and middleware pipeline as your app. It suits scenarios needing custom routing, auth or transforms in .NET. Dedicated proxies remain better for raw throughput and features like caching and WAF.
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
app.MapReverseProxy();
538. How do idempotency keys work at the API layer?
The client sends a unique key with a mutating request, the server stores the key and the response, and repeats return the stored response instead of reprocessing. This makes retrying a POST safe after a network failure. Keys need a scope, a TTL and atomic storage to avoid races.
POST /payments
Idempotency-Key: 9f1c2b7e-4c2a-4d1a-9b0e-2f7c1a3d5e6f
539. How do you implement idempotency keys without race conditions?
Insert the key into a unique-constrained store in the same transaction as the business operation, or use a conditional write that fails if the key exists. If the first request is still in flight, return 409 or 425 rather than duplicating work. Store the response body and status so retries replay identically.
540. What makes a good error contract for a public API?
A stable machine-readable code, a human-readable message, field-level details for validation, and a correlation id for support. Status codes carry the coarse meaning, and the body adds specifics without leaking internals. Keep the shape identical across endpoints and versions.
{
"type": "https://api.example.com/errors/validation",
"title": "One or more validation errors occurred",
"status": 400,
"errors": { "quantity": ["Must be greater than 0"] },
"traceId": "00-8f2c1a..."
}
541. Why should you avoid returning exception messages to clients?
Exception text can expose stack traces, SQL fragments, file paths and internal implementation details useful to an attacker. Log the details with a correlation id and return a generic ProblemDetails message. Only surface deliberately designed domain error messages.
542. How do you configure request timeouts in ASP.NET Core?
Use the request timeout middleware (AddRequestTimeouts and UseRequestTimeouts) for per-endpoint or global limits, and pass CancellationToken through all downstream calls. Client timeouts and proxy timeouts must be aligned, otherwise you get truncated responses. Long-running work belongs in an async job pattern.
builder.Services.AddRequestTimeouts(o =>
o.DefaultPolicy = new RequestTimeoutPolicy { Timeout = TimeSpan.FromSeconds(10) });
app.UseRequestTimeouts();
543. How do you apply a timeout to an outbound HTTP call from an API?
Configure a named or typed HttpClient with a Timeout, and combine it with a CancellationToken from the request so client disconnects propagate. Retries belong in a resilience pipeline with a budget, not in a naive loop. Never share a mutable HttpClient with per-request headers.
544. Why should you avoid exposing EF Core entities directly from controllers?
Entities carry persistence concerns such as navigation properties, lazy-loading proxies and change tracking, which produce oversized payloads, serialization cycles and tight coupling to the database schema. They also invite over-posting, because a client can set properties you never intended to be writable. Map to explicit DTOs so the contract is intentional, stable and safe.
545. How do you read the current environment name and what are the conventional values?
IWebHostEnvironment.EnvironmentName exposes the value, which comes from ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT. The conventions are Development, Staging and Production, and IsDevelopment() checks are extension helpers. The value must never be trusted as a security boundary; it only controls configuration and diagnostics behaviour.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
546. What are the risks of environment-specific appsettings files?
Values drift between environments and a missing override silently inherits the previous layer, which can point production at a development database. Keep only environment-specific overrides in these files and validate critical options at startup. Prefer injecting real values from environment variables or a secret store in production.
547. How do you pick up configuration changes without restarting the application?
File providers support reloadOnChange, and IOptionsMonitor<T>.OnChange notifies listeners with a new value. Consume the current value through CurrentValue instead of caching it in a field. Some settings, such as Kestrel endpoints or DI registrations, still require a restart.
_monitor.OnChange(o => _logger.LogInformation("Limit changed to {Limit}", o.Limit));
548. Should a class library depend on IConfiguration or on IOptions?
Libraries should take a strongly typed options object or plain parameters rather than IConfiguration, so the host owns binding and the library has no file or environment assumptions. This keeps the library testable and reusable outside ASP.NET Core. Expose a binding extension such as AddMyLibrary(Action<MyOptions>).
549. Why is injecting IOptionsSnapshot into a singleton invalid?
IOptionsSnapshot<T> is registered as scoped, so resolving it from a singleton creates a captive dependency and throws when scope validation is enabled. Even without validation it freezes one snapshot for the lifetime of the process. Use IOptionsMonitor<T> in singletons and IOptionsSnapshot<T> in scoped services.
550. How does a middleware short-circuit the pipeline and when is that appropriate?
A middleware simply does not call next and writes the response itself, ending the chain. This is appropriate for authentication failures, rate limiting, cached responses, static files or health endpoints. Short-circuiting must still produce a complete, correct response with headers and status.
551. How do you pass request-scoped data from middleware to a controller?
Store it in HttpContext.Items, which lives for the request, or enrich the response headers. A cleaner approach is a scoped service holding the values, injected into both the middleware and the controller. Avoid static or thread-static fields, which break under async and pooling.
context.Items["CorrelationId"] = correlationId;
// later
var id = HttpContext.Items["CorrelationId"] as string;
552. How do you implement global exception handling with IExceptionHandler in .NET 8 and later?
Implement IExceptionHandler with TryHandleAsync, register it with AddExceptionHandler<T>(), and call app.UseExceptionHandler(). Return true once you have written a ProblemDetails response, or false to let the next handler try. This keeps error mapping in one testable class instead of a lambda.
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
{
ctx.Response.StatusCode = ex is NotFoundException ? 404 : 500;
await ctx.Response.WriteAsJsonAsync(new ProblemDetails { Status = ctx.Response.StatusCode }, ct);
return true;
}
553. What is the difference between UseExceptionHandler and an exception filter?
UseExceptionHandler is middleware that catches anything thrown further down the pipeline, including model binding and serialization failures. Exception filters only wrap action execution inside MVC and cannot catch middleware-level errors. Global error handling is therefore usually built on the middleware.
554. How would you add a correlation id to every request and response?
Read an incoming X-Correlation-Id header or generate one, store it in HttpContext.Items and the logging scope, then echo it on the response via OnStarting. Downstream HTTP calls should forward the same value. This makes logs traceable across services.
var id = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
ctx.Response.OnStarting(() => { ctx.Response.Headers["X-Correlation-Id"] = id; return Task.CompletedTask; });
555. What is HttpContext.Features and can you give a practical use?
Features are the low-level server abstraction collection, exposing things like IHttpRequestBodyDetectionFeature, IHttpResponseBodyFeature or IHttpMaxRequestBodySizeFeature. You can read or replace a feature to change server behaviour for a request. Custom features are also a clean way for middleware to hand data to later components.
556. When must the developer exception page be disabled?
It must never be enabled outside local development because it exposes stack traces, source snippets, configuration values and connection strings. Gate it on IsDevelopment() and ensure the production path uses the exception handler middleware with logging. A misconfigured environment name is a common cause of accidental exposure.
557. How do you conditionally add middleware only for certain environments?
Wrap the registration in an if on app.Environment, or use UseWhen with a predicate when the decision is per request. Keep the branching shallow because two different pipelines are hard to reason about. Prefer configuration flags over environment names when behaviour varies per deployment.
558. How do you generate URLs for other endpoints without hardcoding paths?
Use LinkGenerator or Url.Action with route names and values so links stay correct when templates change. Endpoint names come from [HttpGet(Name = "...")] or WithName in minimal APIs. This matters for pagination links and Location headers.
var url = _linkGenerator.GetPathByName("GetOrder", new { id = 42 });
559. What causes an AmbiguousMatchException in routing and how do you fix it?
Two endpoints match the same request with equal precedence, typically because templates differ only by an optional or unconstrained segment. Routing throws rather than guessing. Fix it by adding a constraint, reordering precedence with literal segments, or merging the endpoints.
560. What is endpoint metadata and how do you read it at runtime?
Routing attaches metadata objects to an endpoint, such as IAuthorizeData, IAllowAnonymous, ProducesResponseTypeMetadata or custom attributes. Middleware can inspect context.GetEndpoint()?.Metadata to make per-endpoint decisions. This is how authorization and CORS policies become endpoint-aware.
561. What are the risks of catch-all route parameters?
A template like {**path} matches everything, so it can shadow other routes depending on precedence and swallow 404s. It also makes caching, authorization and input validation harder because the value is arbitrary. Reserve it for proxy or static-content scenarios and validate the captured path strictly.
562. How do you bind repeated query parameters or a complex filter object?
Bind an array or list, for example ?tag=a&tag=b into string[] tag, or accept a single complex type that the binder populates from query keys. In minimal APIs group many values with [AsParameters]. Validate the count and each value to prevent abuse.
public IActionResult List([FromQuery] string[] tag, [FromQuery] OrderFilter filter) => ...;
563. How do you implement cross-field validation with DataAnnotations?
Implement IValidatableObject on the model and return ValidationResult instances from Validate, optionally naming the offending members. This covers rules like "end date must be after start date". For anything more complex or reusable, a custom ValidationAttribute is cleaner.
public IEnumerable<ValidationResult> Validate(ValidationContext ctx)
{
if (End <= Start) yield return new ValidationResult("End must follow Start", new[] { nameof(End) });
}
564. How do you surface validation errors that originate in the service or domain layer?
Throw a dedicated validation exception carrying a dictionary of field errors, catch it in the exception handler and emit a 400 ValidationProblemDetails. Alternatively return a result object the controller translates. Never return bare strings, and keep the same error shape as model-binding failures.
565. What behaviours does the [ApiController] attribute enable?
It enables automatic 400 responses for invalid model state, binding source inference for complex parameters, required body inference, multipart form inference, and ProblemDetails for error status codes. It also requires attribute routing. Without it, controllers behave like MVC views controllers and silently ignore ModelState errors.
566. How do you return a clean 400 when the JSON body is malformed?
Deserialization failures surface as JsonException or model-state errors with an input formatter error, which [ApiController] converts to a 400 ProblemDetails. If you handle the exception yourself, distinguish it from business errors and avoid echoing the raw payload. Never allow a malformed body to become a 500.
567. How do you configure JSON serialization globally?
Use AddControllers().AddJsonOptions or ConfigureHttpJsonOptions for minimal APIs, and set policies such as camelCase naming, enum-as-string converters, null handling and max depth. Keep the configuration identical across both stacks so mixed applications behave consistently. Also set a reasonable MaxDepth to guard against deep payloads.
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
568. How do you accept snake_case field names from a client?
Set PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower for that endpoint or globally, or annotate individual members with [JsonPropertyName]. Mixing policies across a codebase confuses clients, so prefer a single documented convention per API version. Remember that the naming policy also affects outgoing responses.
569. How should you represent dates and times in an API contract?
Use ISO 8601 with an explicit offset, and prefer DateTimeOffset for instants so the client cannot misinterpret the zone. Store UTC in the database and convert at the edges. Document which fields are instants versus local dates, since a date-only value should not be sent as midnight UTC.
"createdAt": "2026-03-14T09:21:00Z",
"deliveryDate": "2026-03-20"
570. How do you model an operation that is not CRUD, such as cancelling an order?
Prefer a state sub-resource or a control action, for example POST /orders/42/cancel, which is explicit and easy to authorize. If the state is a simple field you may also model it as PATCH on the order. Avoid overloading DELETE when the resource still exists afterwards.
571. How would you design a bulk create endpoint?
Accept a collection at the collection URI with POST /orders/batch, return 200 or 207 with a per-item result array, and enforce a maximum batch size. Decide explicitly whether the operation is atomic or best-effort, and document it. For very large volumes, return 202 and process asynchronously.
572. How do you report partial success without confusing clients?
Return 207 Multi-Status or 200 with an explicit results array where each entry has an index, status and optional error. Keep the overall status meaningful and never hide failures behind a 200 without per-item detail. Clients must be able to correlate results with their input by position or id.
{ "results": [ { "index": 0, "status": 201 }, { "index": 1, "status": 409, "error": "duplicate" } ] }
573. Should child collections be nested in the URL or filtered at the top level?
Nest when the child's identity is scoped to the parent and it cannot exist independently, such as /orders/42/items. Use a top-level resource with a filter when you need to query children across parents, such as /order-items?status=backordered. Deep nesting beyond two levels makes URLs brittle.
574. What is HATEOAS and do most APIs need it?
HATEOAS means responses include links describing available transitions, so clients navigate by following links instead of hardcoding URLs. It improves discoverability but adds payload and complexity, and few public APIs implement it fully. Most teams get most of the benefit from a well-documented OpenAPI contract plus pagination links.
575. How do you design a long-running operation endpoint?
Return 202 Accepted with a Location header pointing at an operation resource that reports status, progress and the eventual result. Include Retry-After and keep the operation resource idempotent to read. This decouples the client from timeouts and server restarts.
HTTP/1.1 202 Accepted
Location: /operations/7f3c
Retry-After: 5
576. How do you implement conditional GET with ETag?
Compute a stable hash or version for the representation, return it in ETag, and compare If-None-Match on later requests to reply 304 without a body. This saves bandwidth and CPU. The ETag must change whenever the representation changes, or clients will serve stale data.
if (ctx.Request.Headers.IfNoneMatch == etag)
{
ctx.Response.StatusCode = StatusCodes.Status304NotModified;
return;
}
577. How does If-Match implement optimistic concurrency at the HTTP level?
The client sends the ETag it last saw in If-Match, and the server rejects the update with 412 Precondition Failed if the current version differs. This prevents lost updates without holding locks. Map it to a rowversion or version column in the database.
578. When should you return 409 versus 422?
409 Conflict means the request is well-formed but conflicts with current server state, such as a duplicate email or a version mismatch. 422 Unprocessable Content means the syntax is valid but the semantics fail business rules. Many teams use 400 for all validation and reserve 422 for semantic failures; the important thing is consistency.
579. What should a DELETE endpoint return and is it idempotent?
Return 204 No Content on successful deletion, 404 if the resource never existed, and consider 200 with a body if you need to return details. Deleting an already-deleted resource should not error on repetition, which is what makes DELETE idempotent in effect. Soft deletes should still hide the resource from reads.
580. How do you deprecate an API endpoint properly?
Announce it in documentation, add Deprecation and Sunset response headers, and keep the old version working for a published window. Monitor usage per client so you know who still calls it. Never remove an endpoint without a migration path.
Deprecation: true
Sunset: Wed, 01 Jul 2026 00:00:00 GMT
Link: <https://api.example.com/v2/orders>; rel="successor-version"
581. How do you publish separate OpenAPI documents per API version?
With Asp.Versioning.ApiExplorer you enumerate the discovered API versions and register one document per version. Each document then contains only the endpoints for that version, which is what client generators expect. Expose them at stable paths such as /swagger/v2/swagger.json.
582. How does media type versioning work in practice?
The client requests a vendor-specific media type that encodes the version, and the server selects the matching formatter or controller. It keeps URLs clean and aligns with content negotiation, but it is harder to test in a browser, harder to cache and poorly supported by some proxies. Many teams still prefer explicit path versioning.
GET /orders/42
Accept: application/vnd.example.order.v2+json
583. How do you keep minimal API registrations organized in a large application?
Group endpoints into static classes with an MapXxxEndpoints(this IEndpointRouteBuilder) extension method, and call them from Program.cs or from route groups. Keep handlers in the same file as their mappings so the contract is readable. This avoids a single thousand-line Program.cs.
public static class OrderEndpoints
{
public static IEndpointRouteBuilder MapOrderEndpoints(this IEndpointRouteBuilder app)
{
var g = app.MapGroup("/api/orders");
g.MapGet("/{id:int}", GetById);
return app;
}
}
584. How do you stream server-sent events or an endless response from an API?
Return an IAsyncEnumerable<T> and set the content type, or write to Response.Body with explicit flushing. Disable response buffering and handle client disconnects through the request cancellation token. Remember that proxies and load balancers may time out idle connections.
app.MapGet("/stream", (CancellationToken ct) =>
TypedResults.Ok(Generate(ct)));
585. How do you integration test a minimal API endpoint?
Use WebApplicationFactory<Program> with a partial Program class, create a client with CreateClient() and call the endpoint over the in-memory test server. Override dependencies such as the database through ConfigureTestServices. Assert on status code, headers and the deserialized ProblemDetails body.
var factory = new WebApplicationFactory<Program>();
var client = factory.CreateClient();
var res = await client.GetAsync("/api/orders/999");
Assert.Equal(HttpStatusCode.NotFound, res.StatusCode);
586. What are the main limitations of minimal APIs compared with controllers?
Minimal APIs lack built-in model-state validation before .NET 10, have no filters hierarchy, and do not support some MVC features such as view results or complex binder customization. They also encourage large handler lambdas if you are not disciplined. Controllers remain stronger for large, convention-heavy APIs.
587. What are keyed services and when are they useful?
Keyed services let you register several implementations of one interface under different keys and resolve a specific one with [FromKeyedServices("key")] or GetRequiredKeyedService. They replace factory delegates and service-locator patterns for choosing a strategy. Common uses are multiple payment providers or storage backends.
builder.Services.AddKeyedSingleton<IPaymentGateway, StripeGateway>("stripe");
builder.Services.AddKeyedSingleton<IPaymentGateway, AdyenGateway>("adyen");
588. Why should you use IHttpClientFactory instead of newing up HttpClient?
The factory pools and rotates handlers, avoiding socket exhaustion from disposing many clients and stale DNS from a single long-lived one. It also centralizes base addresses, default headers, resilience policies and logging. Register typed clients so dependencies stay explicit and testable.
builder.Services.AddHttpClient<CatalogClient>(c =>
c.BaseAddress = new Uri("https://catalog.internal/"));
589. How do you add cross-cutting behaviour to a registered service without editing it?
Use the decorator pattern: implement the same interface, wrap the inner implementation and register the decorator with a factory that resolves the inner type. Libraries such as Scrutor provide Decorate for this. Typical uses are caching, metrics and retry around an existing service.
builder.Services.Decorate<IPriceService, CachedPriceService>();
590. How do you log request and response bodies without leaking secrets?
Log only at Debug or Trace, truncate to a fixed size, and redact known sensitive fields such as passwords, tokens, card numbers and authorization headers. Disable body logging entirely for authentication and payment endpoints. Buffering bodies for logging adds memory and latency, so make it opt-in.
591. How do you control log verbosity per namespace or category?
Set Logging:LogLevel entries per category in configuration, for example raising Microsoft.AspNetCore to Warning while keeping your own namespaces at Information. Category names default to the fully qualified type name, so organizing types into namespaces gives you control. Filters also support rules per provider.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"MyApp.Orders": "Debug"
}
}
}
592. What does OpenTelemetry add over plain logging?
It provides distributed traces, metrics and correlated logs with a vendor-neutral protocol, so you can see a request's full path across services. ASP.NET Core and HttpClient emit activities automatically once instrumentation is added. This makes latency bottlenecks and error propagation visible where logs alone only show isolated events.
593. How do you publish custom application metrics from an API?
Use System.Diagnostics.Metrics.Meter to create counters, histograms or observable gauges, and register the meter with your metrics exporter. Keep tags low-cardinality, since user ids or raw URLs explode the metric series count. Prefer histograms for latency distributions rather than averages.
private static readonly Meter M = new("Orders");
private static readonly Counter<int> Created = M.CreateCounter<int>("orders.created");
594. How do you write a custom health check?
Implement IHealthCheck with a CheckHealthAsync that returns Healthy, Degraded or Unhealthy plus a description. Register it with AddCheck<T>("name", tags: ...) and keep the check fast with its own timeout. Never let a slow dependency turn health checks into a bottleneck.
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext ctx, CancellationToken ct)
=> Task.FromResult(HealthCheckResult.Healthy("queue reachable"));
595. What should a readiness check avoid doing?
It should not perform expensive queries, mutate state, or fail because an optional dependency is down, which would remove healthy instances from rotation. Avoid checking things the instance cannot fix, such as a third-party outage. Keep it cheap enough to run every few seconds.
596. How do you invalidate output cache entries when data changes?
Tag cached responses with Tag("orders") and call IOutputCacheStore.EvictByTagAsync("orders", ct) after a mutation. Eviction is explicit and immediate, unlike relying on expiry. Tag granularly, for example per resource id, to avoid flushing everything on a single update.
await _cache.EvictByTagAsync($"order-{id}", ct);
597. How do you cache data that differs per user?
Include the user or tenant in the cache key, either explicitly or by varying on a claim or header value, and never share a key across identities. Alternatively cache only non-sensitive reference data and keep personalized data out of shared caches. Verify behaviour with two different users, since leaks here are silent.
598. What is over-posting (mass assignment) and how do you prevent it?
Over-posting happens when a client can set properties you never intended to expose, such as IsAdmin or Balance, because the entity is bound directly. Prevent it by binding to a request DTO that contains only writable fields and mapping explicitly. Never rely on ignoring properties at serialization time for security.
public record UpdateUserDto(string DisplayName, string Email); // no IsAdmin
599. How do you guard against SSRF in an endpoint that fetches a user-supplied URL?
Validate the scheme, resolve the host and block private, loopback and link-local address ranges, then re-check after redirects. Ideally route outbound calls through an allow-listed proxy or a fixed set of domains. Also set timeouts and response size limits so the call cannot be used for resource exhaustion.
600. What is the difference between OAuth2 scopes and roles?
Scopes describe what an application is allowed to do on behalf of a user, such as orders.read, and are part of the access token granted to the client. Roles describe what the user is, such as Administrator, and belong to the identity. APIs typically authorize on scopes for coarse access and on roles or claims for finer business rules.
601. How do you handle logout and token revocation with JWTs?
Access tokens are self-contained, so you cannot revoke them before expiry without state; keep lifetimes short and revoke the refresh token instead. For immediate invalidation, check a denylist or a token version claim on each request. Refresh token rotation with reuse detection limits the damage of a stolen token.
602. How do you design API key authentication for server-to-server clients?
Issue long random keys, store only a hash with an identifier prefix, and look the key up by a constant-time comparison. Send them in a dedicated header, never in the query string where they end up in logs. Support rotation with overlapping validity and record last-used timestamps.
603. What is System.Text.Json source generation and why use it?
A source generator produces serialization code at compile time, removing reflection and startup cost, and it enables trimming and AOT. You annotate a partial JsonSerializerContext and pass its options to the serializer. The trade-off is that every type must be declared in the context.
[JsonSerializable(typeof(OrderDto))]
public partial class ApiJsonContext : JsonSerializerContext { }
604. How does response buffering affect streaming endpoints?
By default Kestrel and proxies may buffer, so small writes are not sent until the buffer fills or the response completes. You must flush explicitly and disable buffering where possible to get true streaming. Also check that compression and proxies do not re-buffer the content.
605. How do you unit test a controller action in isolation?
Instantiate the controller with mocked dependencies, call the action, and assert on the returned result object and its value. You do not need the HTTP pipeline for pure logic, which keeps these tests fast. Still add a few integration tests, because mocking misses binding, filters and serialization.
var sut = new OrdersController(new Mock<IOrderService>().Object);
var result = await sut.Get(1);
Assert.IsType<NotFoundResult>(result.Result);
606. What does WebApplicationFactory give you in integration tests?
It boots the real application in memory with its full middleware pipeline, DI container and configuration, then exposes an HttpClient that talks to the test server. You get realistic routing, filters, auth and serialization without opening a port. It also lets you override services and configuration per test class.
607. How do you replace a real dependency in WebApplicationFactory?
Override ConfigureWebHost and call ConfigureTestServices to remove or replace registrations; the last registration wins for a single service. Replace the database with a container or an in-memory provider, and replace outbound HTTP with a stub handler. Keep overrides minimal so tests still resemble production.
builder.ConfigureTestServices(s =>
{
s.RemoveAll<DbContextOptions<AppDbContext>>();
s.AddDbContext<AppDbContext>(o => o.UseSqlite(_connection));
});
608. How do you test authenticated endpoints without a real identity provider?
Add a test authentication scheme that produces a ClaimsPrincipal from configurable claims, and register it as the default scheme in the test host. Then you can assert 401, 403 and success paths quickly. Do not bypass authorization middleware, since the point is to test it.
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", _ => { });
609. How do you assert on a ProblemDetails error response in a test?
Read the content as ProblemDetails or ValidationProblemDetails with the web defaults, then assert on status, title and the errors dictionary. Asserting only on the status code lets contract regressions slip through. Use the same serializer options the API uses.
610. How do you test a handler that returns TypedResults?
You can call the static handler directly and inspect the returned result type, for example Ok<OrderDto> or NotFound, which is fast and avoids the pipeline. Then add one integration test per route to cover binding and serialization. Typed results make the direct assertion type-safe without casting to IResult.
611. How do you decide how much to unit test versus integration test in an API?
Unit test business rules, validators and mapping where logic is dense and dependencies are easy to fake. Integration test the contracts: routing, status codes, auth, serialization and persistence, because that is where most real defects live. Avoid duplicating every unit test as an integration test; each layer should test what only it can break.
612. When should you test against a real Kestrel server instead of TestServer?
TestServer bypasses the network stack, so it cannot catch issues with HTTP/2, TLS, Kestrel limits or real client behaviour. Use a real host for smoke tests of transport-level concerns and for load or contract testing. Keep the fast in-memory tests for everyday feedback.
613. Why do time-dependent tests become flaky and how do you fix them?
Tests that call DateTime.UtcNow or wait on real delays fail around boundaries, time zones or slow machines. Inject TimeProvider and use FakeTimeProvider, and replace real HTTP with a stub HttpMessageHandler instead of sleeping. Deterministic tests document the intended behaviour far better.
services.AddSingleton<TimeProvider>(new FakeTimeProvider(start));
614. Should you map domain objects to DTOs by hand or with a mapping library?
Hand-written mapping is explicit, debuggable and fast, and the compiler catches signature changes. Libraries such as AutoMapper reduce boilerplate for large, repetitive shapes but hide errors until runtime and complicate refactoring. Many teams use explicit mapping for API contracts and a library only for internal projections.
615. How do you avoid over-fetching when building DTOs from EF Core?
Project directly in the query with Select into the DTO so only the needed columns are read and no change tracking is involved. This also avoids loading navigation properties and triggering lazy loads. Verify the generated SQL, since projecting into methods or computed properties can defeat the translation.
var dtos = await db.Orders
.Where(o => o.Status == OrderStatus.Open)
.Select(o => new OrderSummary(o.Id, o.Total, o.CreatedAt))
.ToListAsync(ct);
616. How do you keep request and response DTOs stable as the domain model evolves?
Treat DTOs as a published contract: change them only deliberately, add rather than rename where possible, and version when a change is breaking. Map at the boundary so internal renames do not leak to clients. Contract tests or an OpenAPI diff in CI catch accidental breaks.
617. Should API DTOs be records or classes?
Records give value equality, concise syntax and immutability, which suits response contracts and request payloads. Classes are more flexible when you need mutable binding, parameterless construction or inheritance. For System.Text.Json both work, but records with primary constructors deserialize cleanly and read better in tests.
618. How do you prevent serialization cycles caused by navigation properties?
Do not serialize entities; project into DTOs that contain only the fields the client needs. If you must serialize a graph, configure ReferenceHandler.IgnoreCycles or use [JsonIgnore], but that usually signals a modelling problem. Cycles also inflate payloads dramatically.
619. How should you evolve an enum that is part of a public API contract?
Serialize as strings so added members do not change existing numeric values, and document that clients must tolerate unknown values. Never renumber or reuse an existing member. If removal is required, deprecate the member and keep accepting it for a transition period.
[JsonStringEnumConverter]
public enum OrderStatus { Pending, Paid, Shipped, Cancelled }
620. What is the difference between a request DTO and a command object?
A request DTO describes the wire contract and is shaped by HTTP concerns, while a command describes an intent in the application layer and carries only domain-relevant data. Mapping between them keeps transport changes from leaking into business logic. In small apps they can be the same type, but separating them pays off as the API grows.
Advanced 377 questions
EF Core and data access, performance and diagnostics, distributed systems, messaging, testing, observability, security, modern .NET and architecture.
621. What is the recommended lifetime for a DbContext in a web request, and why should it not be a singleton?
A DbContext is designed to be short-lived and scoped per unit of work, which in ASP.NET Core means one instance per HTTP request. It is not thread-safe, so sharing a singleton across concurrent requests causes change-tracker corruption and unrelated exception types such as "A second operation started on this context". A singleton also accumulates tracked entities forever, producing unbounded memory growth and stale first-level cache reads. Use AddDbContext with the scoped default, or IDbContextFactory<T> when you genuinely need short-lived contexts outside a request scope.
services.AddDbContext<AppDb>(o => o.UseSqlServer(cs));
services.AddDbContextFactory<AppDb>(o => o.UseSqlServer(cs));
622. How does DbContext pooling work and when does it hurt more than it helps?
AddDbContextPool keeps fully initialised context instances in a pool so the internal service provider, model and query caches are reused without re-allocation per request. On reset the pool clears the change tracker and other per-unit state, so the semantics stay equivalent to a fresh context. The benefit is real only for high-throughput services where construction cost is measurable; pooling is incompatible with constructor state that differs per request or with interceptors registered as instance state. Also avoid pooling if you keep references to tracked entities after the context is disposed, because the instance will be reused by another request.
623. Why is a long-lived DbContext considered a bug even in a desktop application?
Long lifetimes mean the change tracker keeps growing and every SaveChanges call scans an ever larger graph, so save performance degrades superlinearly. Identity resolution guarantees that a later query returns the cached instance instead of fresh database values, which silently serves stale data. Memory also grows because tracked entities and their relationship fixup snapshots are rooted. The fix is a context per logical operation, with explicit loading or projection for the data the operation needs.
624. How does the change tracker decide whether an entity is Added, Modified or Unchanged?
For entities returned by a query the tracker creates an entry marked Unchanged and stores original values; later mutation of a property is detected by comparing current against original snapshots during DetectChanges. Entities attached manually via Add become Added, while Attach marks them Unchanged and Update marks them Modified (or Added when the key is default). Disconnected graphs need care because Update marks every property modified, generating wider UPDATE statements than necessary.
db.Entry(order).Property(o => o.Status).IsModified = true;
db.Entry(order).Property(o => o.RowVersion).OriginalValue = dto.RowVersion;
625. When is DetectChanges invoked, and how can it cost you performance?
DetectChanges walks every tracked entity, compares scalar properties with their snapshots and performs relationship fixup, so its cost scales with graph size. It runs implicitly on most public APIs such as SaveChanges, ChangeTracker.Entries() and many query executions. In loops that add thousands of entities, the repeated scans become the dominant cost. Mitigations are batching work, calling ChangeTracker.AutoDetectChangesEnabled = false for bulk load sections and calling DetectChanges once before saving.
626. What is the difference between tracking, no-tracking and identity resolution?
Tracking registers entities in the change tracker so modifications are persisted; no-tracking materialises detached instances and skips that bookkeeping entirely. Identity resolution is the guarantee that a single tracked query graph contains exactly one instance per key, which matters for navigation fixup and reference equality. AsNoTracking disables both tracking and identity resolution, while AsNoTrackingWithIdentityResolution keeps one instance per key without tracking it. Use the latter for read-only queries that build object graphs and rely on reference equality.
627. Why can AsNoTracking cause duplicate objects in an include-heavy query?
Without identity resolution each row is materialised independently, so a parent joined to several children is instantiated once per row. The result is a list containing multiple distinct objects with the same key, which breaks naïve de-duplication and inflates memory. Switching to AsNoTrackingWithIdentityResolution restores a single instance per key at the cost of a temporary identity map during materialisation. Alternatively project into a DTO shape so the database returns exactly the rows you need.
628. How do you read fresh values when an entity is already tracked with stale data?
Query results are not applied over tracked entities, so the tracked instance keeps its old values. Entry(entity).Reload() re-queries that single row and refreshes both current and original values. Entry(entity).GetDatabaseValues() fetches a detached copy you can inspect without overwriting local changes. If you truly need database truth, consider detaching the entity first so the next query materialises a new instance.
629. What is the correct workflow for adding a migration in a CI pipeline?
Migrations should be generated during development with the design-time factory or startup project configured, then reviewed and committed as source code. In CI you apply them with dotnet ef migrations script --idempotent or a migration bundle rather than executing database update against production. Generating migrations at deploy time is discouraged because it hides schema drift and makes the artifact non-deterministic. Always diff the generated SQL and run it against a staging copy first.
dotnet ef migrations add AddOrderIndex
dotnet ef migrations script --idempotent -o deploy.sql
630. Why is applying migrations automatically at application startup dangerous in production?
Multiple instances starting concurrently will attempt the same DDL and either deadlock or fail, and a long-running migration can outlive the startup probe and get the pod killed mid-way. Startup migration also couples schema change to code deploy, so a rollback of the app does not roll back the schema. Prefer a separate, single-run migration step in the release pipeline with a lock and a timeout. Keep migrations backward compatible for at least one release so old and new code can run side by side.
631. How do you make a column rename safe with zero downtime?
Use the expand and contract pattern: add the new column, dual-write from the application, backfill in batches, switch reads, then drop the old column in a later release. Each step is a separate deploy with its own rollback path, so no single moment breaks running code. Renaming in place is a breaking change because old instances still reference the previous name. The same reasoning applies to type changes and nullability tightening.
632. How do you handle a migration that must transform data, not just schema?
Put pure schema changes in migrations and keep heavy data movement in a separate, resumable job. If the transform must ship with the migration, use raw SQL in Up with batched updates to avoid a huge transaction and log growth. Always implement a genuine Down or explicitly document that the migration is irreversible. Test the migration against a production-sized copy, since duration and locking behaviour are the real risks.
633. What does the delete behaviour of a relationship control, and what are the options?
Delete behaviour decides what happens to dependents when a principal is deleted. Cascade deletes children, Restrict blocks the delete, NoAction leaves it to the database, SetNull nulls the foreign key and ClientSetNull only nulls tracked entities in memory. EF Core defaults required relationships to Cascade and optional ones to ClientSetNull. Choose explicitly per relationship, because the default can silently delete business data.
modelBuilder.Entity<Order>()
.HasMany(o => o.Lines)
.WithOne(l => l.Order)
.OnDelete(DeleteBehavior.Cascade);
634. What is the difference between ClientSetNull and SetNull?
SetNull creates an ON DELETE SET NULL constraint, so the database nulls the foreign key even for rows the context does not track. ClientSetNull creates no database action and only fixes up loaded entities in memory, so untracked dependents keep a dangling key. The latter is the default for optional relationships and often surprises developers when orphan rows remain. For integrity, prefer Restrict plus explicit soft delete in most business domains.
635. Why is soft delete usually better than cascade delete in a business system?
Cascades destroy audit history and can silently remove large subtrees through chains of relationships. Soft delete keeps rows for reporting, compliance and undo, and makes deletion a normal update that fits optimistic concurrency. Implement it with a global query filter plus an interceptor that converts deletes into updates. Remember that filters hide data from queries but not from unique indexes, so plan for filtered indexes on the deleted flag.
modelBuilder.Entity<Order>().HasQueryFilter(o => !o.IsDeleted);
636. What is an owned entity type and when should you model a value object that way?
Owned types are mapped as part of the owner's table rather than as separate entities with their own identity, which fits value objects like Money or Address. They are configured with OwnsOne or OwnsMany, and the owner's key propagates into the owned table. Because they have no identity of their own, they cannot be shared between owners and nullability is controlled by the containing column set. Use a separate entity once the concept needs its own lifecycle, queries or references.
modelBuilder.Entity<Customer>().OwnsOne(c => c.Address, a =>
{
a.Property(x => x.City).HasMaxLength(80);
});
637. What are the practical limitations of owned types in EF Core?
Owned collections require a separate table keyed by owner plus a synthetic discriminator, and they cannot be queried independently of the owner. Nullable owned references are awkward because all columns are inline, and EF cannot distinguish "no address" from "address with null fields" without an extra flag. Changing an owned instance is tracked as modification of the owner rather than as its own state machine. For anything with real behaviour and identity, a normal entity is simpler.
638. How do you map a JSON column and query inside it efficiently?
Map to a JSON column with OwnsOne(...).ToJson() for a document shape, or use HasColumnType("jsonb") with a value converter for a raw string. EF Core translates member access on owned JSON to JSON path expressions, so predicates run server-side without deserialising the whole document. For PostgreSQL, add a GIN index on the jsonb column; on SQL Server use computed columns plus indexes, since indexing inside JSON is limited. Aggregating or sorting on arbitrary JSON paths usually forces a scan.
CREATE INDEX ix_order_meta ON orders USING gin (metadata jsonb_path_ops);
639. When would you choose JSON columns over a normalised child table?
JSON suits sparse, schema-flexible payloads that are always read and written as a whole, such as webhook bodies, feature flags or third-party snapshots. Normalised tables win when you filter, join, aggregate or enforce referential integrity on the individual fields. The decisive question is whether the database must reason about the inner values or merely store and return them. Mixed approaches are common: promote the hot fields to real columns and keep the remainder as JSON.
640. What are temporal tables and how does EF Core support them?
SQL Server system-versioned temporal tables keep a full history of row versions in a companion history table, queried with FOR SYSTEM_TIME. EF Core can create them via IsTemporal() and exposes TemporalAsOf, TemporalAll and related operators for point-in-time queries. They give free audit history without triggers, at the cost of extra storage and write overhead. Schema changes on temporal tables are restricted, so plan migrations carefully.
modelBuilder.Entity<Order>().ToTable("Orders", t => t.IsTemporal());
var past = db.Orders.TemporalAsOf(instant).ToList();
641. How do eager, explicit and lazy loading differ, and why is lazy loading risky?
Eager loading pulls related data in the same query via Include, explicit loading issues a separate query on demand via Load, and lazy loading transparently queries when a navigation property is first accessed. Lazy loading is risky because it hides round-trips inside property access, so a loop over a collection produces the classic N+1 pattern and latency you cannot see in the query log. It also requires virtual navigations and proxies, which interfere with serialisation and detached scenarios. In server code prefer explicit projection or eager loading.
642. How do you fix an N+1 problem you discovered in a list endpoint?
Replace the per-item lookup with a single projection that joins the needed data, or batch the ids and issue one query with an IN clause. Include works for straightforward graphs, but projection into a DTO is usually cheaper because it selects only required columns. Verify with query logging that the count of round-trips is constant regardless of page size. Adding a covering index often removes the remaining cost.
var page = await db.Orders.AsNoTracking()
.Where(o => o.CustomerId == id)
.Select(o => new OrderDto(o.Id, o.Total, o.Lines.Count))
.ToListAsync();
643. How can you detect N+1 queries in production rather than in tests?
Instrument the data layer with a diagnostic listener or interceptor that counts commands per logical operation and logs the correlation id with the count. In OpenTelemetry, EF Core emits activity spans per command, so you can alert on services whose span count per request spikes. APM tools such as Application Insights show the dependency call histogram, which reveals the repeated pattern. A cheap safeguard is a debug-only assertion that fails when more than N commands execute in one request scope.
644. What is the cartesian explosion problem with split queries?
Including two or more collections in one SELECT produces the cross product of their rows, so a parent with 10 orders and 10 lines returns 100 rows and duplicates payload. AsSplitQuery issues one query per collection, trading the cross product for multiple round-trips. Split queries are consistent only inside a transaction or snapshot, otherwise concurrent writes can produce an inconsistent graph. Choose per query: use split for multiple large collections, single query for one collection or small data.
var orders = await db.Orders
.Include(o => o.Lines)
.Include(o => o.Payments)
.AsSplitQuery()
.ToListAsync();
645. What is a compiled query and when does it actually help?
A compiled query caches the translation of a LINQ expression into SQL, skipping the model-dependent query pipeline on subsequent executions. It helps only for hot paths executed at high frequency with the same shape, where translation shows up in profiles. EF Core's internal query cache already covers most cases, so gains are modest and mostly in very tight loops. With EF.CompileAsyncQuery the delegate is static, so it cannot capture instance state.
static readonly Func<AppDb, int, Task<Order?>> ById =
EF.CompileAsyncQuery((AppDb db, int id) => db.Orders.FirstOrDefault(o => o.Id == id));
646. How do you safely execute raw SQL with parameters in EF Core?
Use FromSqlInterpolated or FromSql with DbParameter objects so values are sent as parameters rather than concatenated into the text. Interpolated strings passed to FromSqlInterpolated are converted to parameters, which makes them safe by construction. Only use FromSqlRaw with a parameter array, never with string concatenation of user input. Note that raw SQL composing with Include requires the query to be composable, meaning it must start with SELECT and not end with a semicolon.
var orders = await db.Orders
.FromSqlInterpolated($"SELECT * FROM Orders WHERE CustomerId = {id}")
.ToListAsync();
647. When is Dapper a better choice than EF Core for a specific query?
Dapper is a thin mapper over ADO.NET, so it adds almost no overhead and gives exact control over the SQL and result shape. It suits reporting queries, hand-tuned joins, bulk reads and legacy schemas that do not map cleanly to entities. The trade-off is losing change tracking, migrations, LINQ composition and model validation. Many teams mix both: EF Core for writes and aggregates, Dapper for read models.
648. How do savepoints work with EF Core transactions?
Calling BeginTransaction starts a database transaction, and SaveChanges inside it may create a savepoint automatically when multiple batches are sent. You can create and roll back to a savepoint explicitly via CreateSavepoint and RollbackToSavepoint on the transaction. Savepoints let you discard part of the work without aborting everything, which is useful for optional side effects. Not all providers support them, so verify against your database.
await using var tx = await db.Database.BeginTransactionAsync();
await tx.CreateSavepointAsync("beforeAudit");
await tx.RollbackToSavepointAsync("beforeAudit");
await tx.CommitAsync();
649. What is an ambient transaction and how does TransactionScope interact with EF Core?
An ambient transaction is one stored in Transaction.Current so any enlisted resource participates without passing it explicitly. TransactionScope can span multiple DbContexts or a DbContext plus raw ADO.NET, which is useful for cross-context atomicity. It escalates to a distributed transaction when more than one resource enlists, which is slow and often blocked in cloud environments. Prefer a single context with an explicit BeginTransaction whenever possible.
650. How do you implement optimistic concurrency with a rowversion column?
Mark a byte[] property as IsRowVersion so EF includes the original value in the WHERE clause of UPDATE and DELETE. If no row matches, EF throws DbUpdateConcurrencyException and exposes the affected entries. Handle it by reloading database values, re-applying the user's intended changes and retrying, or by surfacing a conflict to the caller. Expose the token to clients so they can send it back as an ETag.
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
await entry.ReloadAsync();
}
651. How do you implement pessimistic locking without breaking connection pooling?
Pessimistic locking requires a held transaction, so open a transaction, issue a locking read such as SELECT ... WITH (UPDLOCK, ROWLOCK) or FOR UPDATE, do the work and commit quickly. Keep the lock window small, because locks block other sessions and long holds cause timeouts and deadlocks. Never hold a lock across an HTTP call or user think time; use a queue or state machine instead. Order lock acquisition consistently to reduce deadlock probability.
SELECT * FROM Inventory WITH (UPDLOCK, ROWLOCK) WHERE Sku = @sku;
652. What does EnableRetryOnFailure change about your code?
The execution strategy automatically retries transient failures such as deadlocks, connection drops and throttling, wrapping the whole unit of work. Because a retry may re-execute the delegate, your code must be idempotent, especially for operations with external side effects. Explicit transactions must be started through Database.CreateExecutionStrategy().ExecuteAsync, otherwise EF throws that the configured strategy does not support user-initiated transactions. Do not retry non-transient errors like constraint violations.
var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var tx = await db.Database.BeginTransactionAsync();
// work
await tx.CommitAsync();
});
653. Why are bulk inserts slow with plain SaveChanges and what are the alternatives?
SaveChanges sends parameterised INSERTs, and even with batching the per-row overhead in the change tracker dominates for tens of thousands of rows. Options are provider bulk copy such as SqlBulkCopy or Npgsql binary import, third-party libraries like EFCore.BulkExtensions, or staging tables plus a set-based MERGE. Disabling AutoDetectChanges and using AddRange gives a modest improvement but is not a substitute for bulk copy. Always measure throughput per second rather than assuming.
654. How does MaxBatchSize affect insert performance and failure behaviour?
EF Core groups statements into batches limited by MaxBatchSize and provider parameter limits, which reduces round-trips dramatically. Larger batches improve throughput but widen the transaction and increase memory and lock duration. When one statement in a batch fails, the whole batch fails and EF must work out which entries succeeded, so error handling becomes coarser. Tune the value against real payload sizes rather than leaving large defaults unexamined.
655. How do you decide which indexes to create for a slow query?
Read the actual execution plan, find the operator consuming the most cost, and check whether scans are caused by missing or non-sargable predicates. Design composite indexes with equality columns first and the range or sort column last, and include covering columns to avoid key lookups. Verify with the plan that the index is actually used, then measure logical reads before and after. Every index adds write cost and storage, so remove unused ones.
CREATE INDEX ix_orders_customer_created
ON Orders (CustomerId, CreatedAt DESC) INCLUDE (Total);
656. Why can a query be slow even when the correct index exists?
Common causes are parameter sniffing with a plan built for atypical values, stale statistics, implicit conversions that make the predicate non-sargable, and functions applied to the indexed column. Skewed data distribution can also make one cached plan wrong for most executions. Investigate with the actual plan and sys.dm_exec_query_stats rather than guessing. Fixes include updating statistics, rewriting the predicate, or using OPTION (RECOMPILE) for genuinely variable queries.
657. How does keyset pagination work and why is it better than OFFSET at scale?
Keyset pagination filters on the last seen sort key, for example WHERE (CreatedAt, Id) < (@ts, @id) ORDER BY CreatedAt DESC, Id DESC, so the engine seeks directly to the position. OFFSET must generate and discard all preceding rows, so cost grows linearly with page depth. Keyset requires a stable, unique tiebreaker and cannot jump to an arbitrary page number. It also fits infinite scroll far better than a page counter.
SELECT Id, CreatedAt FROM Orders
WHERE (CreatedAt, Id) < (@ts, @id)
ORDER BY CreatedAt DESC, Id DESC
LIMIT 50;
658. What makes a pagination query unstable and how do you prevent it?
Sorting on a non-unique column means rows with equal values can appear on multiple pages or be skipped entirely. Always add a unique tiebreaker such as the primary key to the ORDER BY and to the keyset predicate. Also beware of concurrent inserts shifting offsets, which OFFSET pagination cannot handle. If the UI needs a total count, compute it separately and accept eventual consistency.
659. How does connection pooling behave when a connection is not disposed?
The pool has a maximum size; leaked connections are never returned, so the pool exhausts and new requests block until the connection timeout fires. Symptoms are timeouts with a healthy database and rising active session counts. Always dispose connections and contexts with using or await using, and check for async-over-sync calls that hold connections longer than needed. Enable pool exhaustion metrics and log a stack trace when acquisition exceeds a threshold.
660. What are the important connection string settings for pooling in high-throughput services?
Max Pool Size caps concurrency to the database, Min Pool Size avoids cold starts, and Connection Lifetime helps with failover by recycling old connections. Connection Idle Timeout trims idle connections, which matters for serverless and bursty workloads. Typically the right pool size is far smaller than the thread count, because the database is the bottleneck. Pair these with command timeouts so a stuck query cannot pin a connection forever.
661. How would you design a repository layer over EF Core without hiding its power?
A useful repository exposes intent-level operations for an aggregate, such as GetById, Add and a couple of purpose-built queries, rather than a generic CRUD interface over every entity. Keep IQueryable out of the public surface, because leaking it lets callers build arbitrary queries and defeats the abstraction. Allow read models to be queried directly with projections from the composition root. The point is testability and a clear boundary, not shielding developers from SQL.
662. What are the trade-offs of the specification pattern with EF Core?
Specifications package reusable query criteria and can be composed into expressions, which helps keep query logic out of controllers and services. The cost is indirection: generated SQL becomes harder to predict and includes and projections may not compose cleanly. Overuse leads to a parallel expression language that duplicates LINQ. Use them for genuinely reused, domain-meaningful filters, and keep complex reporting queries explicit.
663. How do you define an aggregate boundary when using EF Core?
An aggregate is a consistency boundary: everything inside is loaded and saved together, and external references go through the root's identity only. Keep aggregates small, because large graphs cause concurrency conflicts, heavy queries and lock contention. Reference other aggregates by id rather than navigation, and use eventual consistency between them. EF Core mapping should follow the model, not dictate it, so avoid the temptation to map every relationship for convenience.
664. What are the semantics of a unit of work with EF Core, and where do people get it wrong?
The DbContext is the unit of work: it tracks changes and flushes them atomically on SaveChanges, which itself wraps the batch in a transaction. Mistakes include calling SaveChanges inside loops, spreading one business operation over several contexts, and committing before external side effects succeed. A correct unit of work has one commit point per use case and emits integration events only after that commit. Outbox patterns bridge the gap when messaging must be atomic with the database.
665. How do memory, distributed and HTTP caching differ in an application architecture?
In-memory caching is per process and fastest but inconsistent across instances; distributed caching like Redis is shared and survives restarts but adds a network hop. HTTP caching with ETag and Cache-Control delegates freshness decisions to clients and proxies, which removes load entirely but requires careful invalidation. Layer them: HTTP for public GETs, distributed for cross-instance shared data, memory for tiny hot lookups with short TTLs. Every layer needs an explicit invalidation strategy.
666. How do you invalidate a distributed cache safely?
Prefer versioned keys or short TTLs over broadcast deletes, because delete messages can be lost or reordered. Write-through with a version stamp means readers compute the key from the current version and old entries expire naturally. If you must delete, publish an invalidation event through a reliable bus and accept a brief window of staleness. Never delete before the database commit, or readers will repopulate with stale data.
667. What Redis data structures would you use for a leaderboard and a rate limiter?
A sorted set gives O(log n) ranking with ZADD and range queries for leaderboards and pagination by rank. A rate limiter maps naturally to a counter with expiry, or to a sorted set of timestamps for sliding windows. Use Lua scripts to make check-and-set atomic, since separate commands can interleave under concurrency. Set TTLs on every key to avoid unbounded growth.
local c = redis.call('INCR', KEYS[1])
if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
return c
668. What are the common Redis caching anti-patterns in .NET services?
Storing large object graphs as one blob causes serialisation cost and thundering-herd rebuilds on expiry. Using KEYS in production blocks the server, so use SCAN instead. Ignoring connection multiplexing by creating a client per request exhausts sockets. Also avoid caching per-user data under shared keys, which leaks data across users.
669. When is BenchmarkDotNet the right tool and what does it protect you from?
BenchmarkDotNet runs warmed-up, statistically analysed iterations across processes, so it removes JIT, tiering and noise artifacts that make manual stopwatch measurements misleading. Use it to compare implementations of a hot path, not to measure end-to-end service latency. Always validate results with the reported error margins and run in Release. Beware of dead-code elimination, which requires consuming results, typically via a return value or Consumer.
[Benchmark]
public int Parse() => int.Parse("12345");
[GlobalSetup] public void Setup() { }
670. How do you use dotnet-counters to confirm a suspicion about GC pressure?
Attach with dotnet-counters monitor and watch allocation rate, gen0 and gen2 collection counts, heap size, GC pause percentage and thread pool queue length. A high allocation rate with frequent gen2 collections points to large object allocations or long-lived caches. Rising thread pool queue length with low CPU indicates blocking or starvation. Counters tell you where to look; they do not explain why.
dotnet-counters monitor -p 1234 --counters System.Runtime
671. When would you reach for dotnet-trace instead of dotnet-counters?
Counters give cheap aggregate signals, while traces capture events over time so you can see call stacks, allocation sites and contention. Use dotnet-trace collect with the CPU sampling or GC provider when you need to know which method burns CPU or allocates. The resulting nettrace file opens in Visual Studio or PerfView for analysis. Keep collection windows short and targeted, since tracing adds overhead.
672. How do you approach a memory leak investigation with dotnet-dump?
Capture two dumps separated by time under similar load, then compare the heap using dumpheap -stat and gcroot to find what grew and who roots it. Look for unexpected counts of a type rather than only total size, because a leak is often many small objects. gcroot traces the path to a static, event handler or thread that keeps the object alive. Confirm the hypothesis in code before changing anything.
dotnet-dump collect -p 1234 -o leak.dmp
dotnet-dump analyze leak.dmp
> dumpheap -stat
673. What are typical managed memory leak causes in long-running .NET services?
Event handler subscriptions never unsubscribed, static collections used as caches without eviction, and captured closures in long-lived timers or background tasks. Unbounded channels and concurrent dictionaries keyed by tenant or request id are frequent culprits. Async state machines can also root objects when a task never completes. Use weak references or explicit disposal, and add memory ceilings with metrics and alerts.
674. How do workstation and server GC differ, and how do you choose?
Workstation GC uses one heap and one GC thread and suits client apps with small heaps and low core counts. Server GC creates a heap per core with parallel collection, favouring throughput on multi-core servers at the cost of higher memory. Container deployments should set DOTNET_GCConserveMemory or use System.GC.HeapCount to avoid one heap per host core. Measure pause times as well as throughput before switching.
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
675. What is the Large Object Heap and what problems does it cause?
Objects of 85000 bytes or more go to the LOH, which is collected only in gen2 and is not compacted by default. Repeated large allocations fragment the heap, raising memory usage and pause times. Buffer pools and ArrayPool avoid churn, and GCSettings.LargeObjectHeapCompactionMode can compact once when necessary. Streaming APIs are better than buffering entire payloads.
676. How do you diagnose thread pool starvation?
The signature is high latency with low CPU, a growing thread pool queue and injection of new threads at roughly one per second. Causes are sync-over-async calls such as .Result or .Wait(), blocking locks held during I/O, and long-running synchronous work on pool threads. Capture a trace and look for threads blocked in Monitor.Wait or waiting on tasks. Fix by removing blocking calls and using async all the way, not by raising MinThreads as a permanent remedy.
677. Which allocations would you target first when optimising a hot path?
Start with the highest-volume small allocations: boxing of value types, string concatenation in loops, closures capturing variables, and LINQ iterators in tight loops. Then check for large temporary arrays and buffered reads that could stream instead. Use the allocation profiler to rank by bytes and by count, since high-count small objects drive GC frequency. Optimise only code proven hot by measurement.
678. How does boxing show up in performance profiles and how do you avoid it?
Boxing allocates a heap object for a value type whenever it is converted to object or an interface, so profiles show allocations attributed to the call site rather than the type. Common sources are non-generic collections, string.Format with value arguments, and interface-typed parameters. Generic APIs and Span<T> avoid it. Watch for implicit conversions in logging and dictionary lookups.
// boxing
object o = 42;
// no boxing
void Log<T>(T value) { }
679. What string handling practices matter most for throughput?
Prefer StringBuilder or interpolation handlers over repeated concatenation, and use string.Create for allocation-free formatting. Compare with StringComparison.Ordinal for non-linguistic data, which is much faster and avoids culture surprises. ReadOnlySpan<char> slicing avoids substrings entirely. For high-volume parsing, avoid Split, which allocates arrays and strings.
680. How do Span and Memory enable allocation-free parsing?
Span<T> is a stack-only view over contiguous memory, so slicing a buffer creates no allocation. Memory<T> is the heap-friendly counterpart that can be stored in fields and used across awaits. Together with Utf8Parser and SequenceReader they let you parse bytes in place. The restriction is that Span cannot cross an await boundary.
ReadOnlySpan<byte> span = buffer.AsSpan(0, read);
Utf8Parser.TryParse(span, out int value, out int consumed);
681. When are System.IO.Pipelines a better abstraction than a Stream?
Pipelines separate reading from processing, so the transport thread can fill buffers while your parser works on them, which suits protocol servers and high-throughput ingestion. They handle partial frames naturally through ReadOnlySequence and back-pressure via flow control. Streams are simpler for file and sequential I/O and integrate with existing APIs. Reach for pipelines when you own the protocol parsing and need to minimise copies.
682. How does ObjectPool help and where does it introduce risk?
Object pooling reuses expensive instances such as large buffers or serialisers, cutting GC pressure and allocation cost. The risk is state leakage: a pooled object returned dirty will corrupt the next user, so always reset on return. Pools also retain memory, so size them and consider trimming. For plain byte arrays, ArrayPool<byte> is usually enough.
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try { /* use */ }
finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }
683. How do source generators improve .NET application performance?
Source generators run at compile time and emit code, so work that would otherwise happen via reflection or runtime expression compilation disappears from startup and steady state. Examples are System.Text.Json source generation, regex generators, and logging generators that avoid boxing and parsing. The benefit is measurable in startup time, AOT compatibility and allocations. The cost is build time and harder debugging of generated code.
[JsonSerializable(typeof(OrderDto))]
internal partial class AppJsonContext : JsonSerializerContext { }
684. What breaks when you enable trimming or Native AOT?
Reflection, dynamic assembly loading, Activator.CreateInstance on unannotated types, and serialisers that discover members at runtime all fail once unused code is removed. Libraries must be trim-annotated; otherwise you get warnings and runtime MissingMethodException. Configuration binding, DI scanning and some ORM features are common casualties. Test the published trimmed build, not just the debug build.
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
685. What are the practical limits of Native AOT for a typical web API?
AOT gives fast startup, low memory and small containers, but it forbids runtime code generation, so dynamic LINQ, some serialisers and reflection-heavy ORMs are out. EF Core has limited AOT support, and compiled queries must be pre-generated. Build times and cross-compilation constraints also affect the pipeline. Evaluate AOT for small, latency-sensitive services rather than large line-of-business apps.
686. How do you reduce application startup time?
Measure first with the startup hook or dotnet-trace, since most time is usually in JIT, assembly loading, DI graph construction and configuration or network calls. ReadyToRun images cut JIT cost, and reducing the number of assemblies and reflection-based registrations helps further. Move non-critical initialisation off the startup path into background services. Avoid synchronous calls to external systems during startup.
dotnet publish -c Release -p:PublishReadyToRun=true -r linux-x64
687. What is ReadyToRun and what does it not do?
ReadyToRun precompiles IL to native code at publish time, reducing JIT work and improving cold start while remaining portable across platforms when published per RID. It does not eliminate the runtime or enable full AOT, and the native code may still be less optimised than tiered JIT output. Binaries are larger. Combine it with tiered compilation settings tuned for your workload.
688. How do you set and defend a performance budget for a service?
Express budgets as user-visible outcomes: p95 latency, error rate, throughput per instance, memory ceiling and startup time. Derive them from product requirements and traffic forecasts, then allocate the budget across layers so no single dependency consumes it all. Enforce them in CI with load tests and fail the build on regression beyond a threshold. Revisit after each major architectural change.
689. How should you design a load test to be meaningful?
Model the traffic shape from production: realistic read and write ratios, payload sizes, think times and ramp-up rather than a flat hammer. Separate load testing from stress testing and soak testing, because each answers a different question. Include downstream dependencies or faithful stubs, since a mocked database hides the real bottleneck. Record p50, p95, p99 and error rate together, and keep a baseline run for comparison.
690. What is the difference between p95 and average latency, and why does it matter?
The average smooths over the tail, so a fast majority hides the slow requests that users actually complain about. p95 means 5 percent of requests are slower than that value, which is a better proxy for perceived performance. Report several percentiles plus the max within a window, and keep the sample count visible. Never compare p95 across systems with very different traffic volumes.
691. How do you profile database round-trips in a distributed request?
Correlate the request id across application logs, EF Core command logs and database-side activity to reconstruct the timeline. OpenTelemetry spans for each command show duration and SQL text, and the trace waterfall exposes calls that were hidden inside loops. Server-side tools such as Query Store or pg_stat_statements add real execution time independent of the client. Compare the two to separate network latency from database work.
692. How would you capacity plan for a service that must sustain a traffic peak?
Measure throughput per instance under a realistic load test, then add headroom for failover, GC pauses and rolling deploys. Identify the binding constraint first, which is usually the database or a downstream dependency rather than the app tier. Model peak-to-average ratios from historical data and plan for the failure of one availability zone. Validate with a game day that removes an instance during peak.
693. What is query plan caching and how can it produce unpredictable latency?
The database caches a compiled plan per statement shape, so the same query text reuses a plan built for the first parameter values it saw. With skewed data that plan can be optimal for one tenant and catastrophic for another. Symptoms are queries that are fast in isolation but slow in production for specific inputs. Solutions include plan guides, OPTION (RECOMPILE), or splitting hot and cold code paths.
694. How do you keep an EF Core query sargable?
Keep the indexed column alone on one side of the comparison and avoid functions, arithmetic or casts on it. Convert computed predicates into range predicates where possible, for example CreatedAt >= start && CreatedAt < end instead of truncating the date. Ensure parameter types match the column types so no implicit conversion occurs. Verify by checking that the plan shows a seek rather than a scan.
// non-sargable
.Where(o => o.CreatedAt.Year == 2026)
// sargable
.Where(o => o.CreatedAt >= new DateTime(2026,1,1) && o.CreatedAt < new DateTime(2027,1,1))
695. Which LINQ constructs silently fall back to client evaluation in EF Core?
Methods EF cannot translate, custom C# functions, some string overloads without comparison arguments and complex projections can force client evaluation or throw. Modern EF Core throws for the final projection in most cases, but a partially client-evaluated subquery may still pull excessive rows. Read the warnings logged at Warning level during development. Keep predicates translatable and do post-processing in memory explicitly.
696. How do you avoid loading an entire table when only a few columns are needed?
Project into a DTO or anonymous type so the generated SQL selects only the required columns. This also avoids tracking and bypasses the identity map, which is faster and safer for read paths. Use AsNoTracking when the full entity is genuinely needed. Check the generated SQL, because a projection that references navigation properties can still join broadly.
697. Why can Include make a query slower than separate queries?
Each Include adds joins and duplicate parent columns, so wide rows and cartesian products inflate network traffic and materialisation cost. Multiple collection includes are the worst case, and they may also break pagination semantics. Split queries or explicit projection are often cheaper. Measure the actual SQL and row counts rather than assuming Include is free.
698. How does EF Core handle transactions when SaveChanges spans multiple batches?
When more than one batch is required, EF Core creates a transaction automatically so the whole save is atomic. If an ambient transaction already exists, it participates in that instead. Providers that support savepoints use them so a failed batch can be rolled back without discarding earlier work. Disabling automatic transactions is possible but rarely wise.
699. What is the risk of calling SaveChanges inside a loop?
Each call opens a transaction, flushes a batch and re-runs change detection, so cost multiplies with the number of iterations. Partial failure also leaves the data half-written unless you wrap the loop in an explicit transaction. Prefer mutating the graph and saving once at the unit-of-work boundary. If you need streaming progress, commit in explicit chunks.
700. How do interceptors help with cross-cutting data concerns?
EF Core interceptors let you hook command execution, connection opening, SaveChanges and transaction boundaries without polluting domain code. Typical uses are auditing timestamps, soft delete, slow query logging and injecting tenant filters. Register them through AddInterceptors and keep them fast, because they run on every operation. Prefer them over overriding SaveChanges when the behaviour must also apply to raw SQL.
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData e, InterceptionResult<int> r, CancellationToken ct)
=> base.SavingChangesAsync(e, r, ct);
701. How would you implement multi-tenancy at the data layer?
For a shared schema, add a tenant id column, apply a global query filter from the current tenant context and enforce it in the database with row-level security if available. For stronger isolation use a database or schema per tenant with a tenant-aware connection resolver. Whichever you choose, resolve the tenant from an authenticated claim, never from a client-supplied header alone. Add tests that prove cross-tenant reads are impossible.
702. Why is a global query filter not a security boundary on its own?
The filter applies to LINQ queries but not to raw SQL, IgnoreQueryFilters calls or direct database access. Anyone with connection credentials can bypass it, and a single developer mistake removes the filter. Treat it as defence in depth and enforce real authorisation in the database or at the API layer. Also remember filters affect migrations and seed data.
703. How do you model a many-to-many relationship with payload in EF Core?
Use an explicit join entity when the link carries data such as quantity or timestamps; EF Core then maps it as two one-to-many relationships. Skip navigations with an implicit join table are fine only for pure associations. Query through the join entity for the payload and keep indexes on both foreign keys. Watch out for cascade paths when both sides are required.
704. What causes the "multiple cascade paths" error and how do you resolve it?
SQL Server refuses a schema where a row can be deleted through two different cascade chains, because the outcome is ambiguous. It appears when an entity is reachable from a principal by more than one required relationship. Resolve it by making one path Restrict or NoAction and handling deletion explicitly in code. Another option is to break the relationship into an optional one.
705. How do you handle concurrency conflicts across aggregates without long locks?
Keep transactions short, use optimistic concurrency tokens and design operations to be idempotent so retries are safe. Where contention is inherent, serialise through a queue or partition the work by key so conflicts are rare. Escalate to pessimistic locking only for short critical sections. Measure conflict rates rather than guessing.
706. What is the difference between a deadlock and a lock timeout, and how do you respond to each?
A deadlock is detected by the database, which kills one victim transaction immediately; a lock timeout occurs when a waiter exceeds the configured threshold. Deadlocks usually indicate inconsistent access order or missing indexes causing scans. Timeouts usually mean long-running transactions or excessive blocking. Both should be retried only when the operation is idempotent.
707. How do you make a retry policy safe for operations with side effects?
Make the operation idempotent using a natural key, an idempotency token or a deduplication table, so a replay produces the same outcome. Record intent before performing the side effect, then confirm completion afterwards, which is the basis of the outbox and inbox patterns. Never retry blindly on an ambiguous timeout without checking state. Bound retries with exponential backoff and jitter.
708. How do you decide between a database transaction and a saga?
Use a local transaction when all state lives in one database and can commit atomically. When a business operation spans services or external systems, use a saga with compensating actions, because distributed transactions are fragile and often unsupported. Sagas require explicit design for partial failure and intermediate states visible to users. Keep the number of steps small and each step idempotent.
709. What does the outbox pattern solve and how is it implemented?
It makes database changes and message publication atomic by writing the message into an outbox table inside the same transaction, then publishing asynchronously. A background relay reads unpublished rows, sends them, and marks them done, with at-least-once semantics requiring consumer idempotency. This avoids the dual-write problem where the commit succeeds but the publish fails. Use ordering keys and a retention policy for the outbox table.
710. How would you detect and fix a slow first request after deployment?
Cold start comes from JIT, assembly loading, model building and connection pool warm-up. Fix by publishing ReadyToRun images, pre-warming the EF model, and running a warm-up probe that hits critical paths before the instance receives traffic. Keep startup dependencies minimal and lazy-load optional features. Measure with traces that separate startup from first-request work.
711. How do you reduce EF Core model-building cost at startup?
Model building happens once per context type per service provider, so the main cost is a very large model or many context types. Compile the model ahead of time with a generated IModel and register it via UseModel. Reduce the number of distinct context types and avoid heavy conventions. Measure model build time in traces before optimising.
optionsBuilder.UseModel(AppDbContextModel.Instance);
712. What are the trade-offs of splitting a large DbContext into several smaller ones?
Smaller contexts build faster, have clearer boundaries and reduce the chance of accidental cross-aggregate navigation. The cost is more registrations, more model builds and the loss of easy joins across the split. Cross-context operations need explicit transactions or eventual consistency. Split along aggregate or module boundaries, not arbitrarily.
713. How do you test data access code without making tests slow and flaky?
Use integration tests against the real provider, typically a containerised database, because in-memory providers do not reproduce SQL semantics, constraints or concurrency. Keep a small number of fast unit tests for domain logic and a broader suite for repositories. Reset state between tests with transactions rolled back or a fresh schema. Seed with builders so tests read clearly.
714. Why is the EF Core in-memory provider unsuitable for meaningful tests?
It is a LINQ-to-objects store, so it does not enforce relational constraints, does not translate to SQL and accepts queries the real database would reject. Tests pass locally and fail in production on constraint violations or unsupported translations. SQLite in-memory is a better approximation if you need speed. Use the production provider for anything that must be trusted.
715. How do you keep read models consistent when using CQRS with separate stores?
Publish domain events transactionally through an outbox, then project them into the read store with idempotent handlers and a checkpoint per consumer. Accept eventual consistency and expose it in the API through versioning or "read your writes" tokens where needed. Monitor projection lag as a first-class metric. Be able to rebuild projections from the event log.
716. When is a document database a better fit than a relational one?
Document stores fit aggregate-shaped data read and written as a unit, with rapidly evolving schemas and horizontal scale-out requirements. Relational databases win when you need ad-hoc joins, strong constraints and mature transactional guarantees. The deciding factor is the query and consistency model, not developer preference. Many systems use both, with relational as the source of truth.
717. How do you approach a query that got slower after a data volume increase?
Compare the current plan with the historical one, because the optimiser may have switched from seek to scan once statistics changed. Check for missing indexes on new access patterns, key lookups caused by non-covering indexes and parameter sniffing. Update statistics and consider filtered indexes for skewed subsets. Then verify with logical reads, not just elapsed time.
718. How do you handle large exports without exhausting memory?
Stream results with AsAsyncEnumerable or a data reader rather than materialising a list, and write the output incrementally to the response or storage. Disable tracking and avoid buffering middleware for that endpoint. Consider keyset batching so each chunk is a bounded query. Always apply a timeout and cancellation token.
await foreach (var row in db.Orders.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct))
{
await writer.WriteAsync(row.ToCsvLine());
}
719. What are the risks of streaming a response while holding a database connection?
The connection stays checked out for the whole response, so slow clients occupy pool slots and can starve other requests. Network interruptions surface as exceptions mid-stream, after headers were sent, so the client sees a truncated payload. Mitigate with bounded chunk sizes, timeouts and a connection-per-chunk strategy. Monitor connection pool saturation during large exports.
720. How do you choose between a surrogate key and a natural key in a relational model?
Surrogate keys are stable, narrow and immune to business rule changes, which makes them better for clustering and foreign keys. Natural keys carry meaning and can avoid an extra lookup but often change, which cascades through the schema. A common compromise is a surrogate primary key with a unique constraint on the natural key. Keep keys narrow because every non-clustered index carries them.
721. Why does key width matter for index performance?
Every non-clustered index stores the clustering key, so a wide key inflates all indexes and the buffer pool footprint. Wide keys also slow comparisons and increase fragmentation. Prefer narrow integer or sequential GUID types and avoid GUID primary keys with random distribution on clustered indexes. If GUIDs are required, use sequential generation.
722. What problems do random GUID primary keys cause in SQL Server?
Random values scatter inserts across pages, causing page splits, fragmentation and poor cache locality. The clustered index grows in all directions, and index maintenance becomes heavier. Sequential GUIDs or a separate identity column with a unique GUID column solve it. The same concern applies to any non-monotonic clustered key.
723. How do you safely deploy a schema change that rewrites a large table?
Avoid long-held locks by making the change online where the engine supports it, or by using a shadow table and batched copy. Batch the backfill with small transactions and throttling so replication and locking stay healthy. Deploy in expand and contract steps rather than one atomic rewrite. Have a rollback plan that does not require restoring the whole database.
724. How do you add a NOT NULL column to a large existing table?
Add it as nullable with a default, backfill in batches, then tighten to NOT NULL in a later migration once all rows are populated. Adding NOT NULL with a default on a large table can rewrite every row and hold a lock. For engines that support metadata-only default constraints the operation is cheap, but do not assume it. Verify behaviour on a production-sized copy.
725. What is the role of statistics in query performance?
Statistics describe value distribution, and the optimiser uses them to estimate cardinality and pick join strategies and index usage. Stale statistics after bulk loads cause bad estimates, which produce nested loops over millions of rows or hash joins over ten. Update statistics after large changes and consider sampled versus full scans. Ascending key columns often need special attention because new values fall outside the histogram.
726. How do you read an execution plan to find the real bottleneck?
Start with the operator that has the largest cost relative to actual rows, then compare estimated versus actual row counts to detect bad cardinality estimates. Look for scans where a seek is expected, key lookups, sort and hash spills to tempdb, and implicit conversions. Read the plan right to left but judge by actual rows, not the percentage cost. Always use the actual plan for diagnosis.
727. What does a key lookup in a plan tell you?
It means the index satisfied the predicate but not the selected columns, so the engine had to fetch rows from the clustered index. A few lookups are harmless, but thousands indicate a non-covering index that should include the requested columns. Alternatively the query selects too many columns and should be projected narrower. Lookups are especially costly when they cause random I/O.
728. How do you decide whether to denormalise for read performance?
Denormalise when a measured read workload cannot meet its budget through indexing or caching, and the duplication has a clear owner. Accept the cost of keeping copies consistent, usually through the same transaction or an outbox-driven projection. Prefer materialised read models over scattering duplicate columns. Document the invariant that keeps the copies aligned.
729. What is the difference between a covering index and an included column?
Key columns define order and are usable for seeking, sorting and filtering; included columns exist only in the leaf level to satisfy the projection. Including a column avoids a key lookup without widening the seek structure. Include wide columns rather than making them keys. Keep the index narrow enough that maintenance stays cheap.
730. How does the buffer pool influence apparent query performance?
Queries reading cached pages avoid disk I/O, so the same query can be fast in a warm system and slow after a restart. Memory pressure from other queries or a large scan can evict hot pages and cause sudden latency spikes. Monitor page life expectancy and cache hit ratio alongside logical and physical reads. Do not tune based on a single cold run.
731. How would you investigate intermittent latency spikes that do not correlate with traffic?
Look for background causes: GC pauses, index or statistics maintenance, backup and log growth, autovacuum, container CPU throttling and thread pool injection after a burst. Correlate spikes with infrastructure metrics and GC counters on the same timeline. Check for connection pool churn causing new physical connections. Sustained investigation needs continuous profiling rather than a one-off capture.
732. What is CPU throttling in containers and how does it appear to .NET?
When a container exceeds its CPU quota within a scheduling period it is paused, so wall-clock latency rises while CPU usage looks moderate. .NET sees time jumps, and thread pool and timer behaviour degrade because the process is simply not scheduled. Symptoms are latency spikes with low average CPU and high throttling counters. Fix by raising limits, reducing bursty allocations or lowering thread counts.
733. How does .NET behave when a container has a CPU limit?
The runtime reads cgroup limits and sizes the thread pool and server GC heaps accordingly, so the process does not assume all host cores are available. This is why container-aware configuration matters and why older runtimes over-allocated threads and memory. Verify by inspecting the reported processor count at startup. Never override the detected count without measuring.
734. What is tiered compilation and how does it affect benchmark results?
Tiered compilation starts with quickly generated code and recompiles hot methods with optimising JIT, so performance improves over the first seconds or minutes. Benchmarks must therefore run long enough after warm-up, which BenchmarkDotNet handles automatically. In short-lived processes the fast tier may dominate and hide the steady-state behaviour. Disable tiering only when you have a specific reason.
735. What is PGO and when would you enable it?
Dynamic PGO lets the runtime collect type and branch profiles and recompile hot code with better devirtualisation and inlining. It is enabled by default in modern .NET and generally helps, but can add startup cost and code size. Measure with and without for your workload before changing defaults. Static PGO via ReadyToRun is a different mechanism aimed at cold start.
736. How do you reduce allocation in an ASP.NET Core request pipeline?
Minimise per-request services, avoid logging with string interpolation on hot paths and use the generated LoggerMessage pattern. Reuse buffers with pooling, and avoid reading entire bodies into strings. Keep middleware lightweight and do not allocate closures in filters. Measure allocations per request with a profiler rather than guessing.
737. How do you cache serialised responses without serving stale or private data?
Separate public caches from authenticated responses, and set Cache-Control with private or no-store accordingly. Use ETags for conditional requests so clients revalidate cheaply. Invalidate by versioning the cache key when underlying data changes. Never cache a response keyed only by URL if it varies by user or tenant.
Response.Headers.CacheControl = "public,max-age=60";
Response.Headers.ETag = etag;
738. What is a thundering herd on cache expiry and how do you prevent it?
Many requests miss the same key simultaneously and all recompute the value, saturating the database. Use a per-key lock or a "stale while revalidate" pattern so one caller refreshes while others serve the old value. Add jitter to TTLs to spread expiries. For expensive aggregates, precompute on a schedule instead of on demand.
739. How do you choose a cache TTL for data that changes unpredictably?
Derive the TTL from the business tolerance for staleness rather than from a guess about change frequency. Add explicit invalidation for events you can observe, and keep TTLs as a safety net. Shorter TTLs raise load, longer ones raise staleness risk. Instrument cache hit ratio and staleness complaints to tune empirically.
740. What is cache stampede protection at the distributed layer?
Distributed locks or single-flight primitives ensure only one node recomputes a missing value while others wait or serve a default. Redis SET NX with a short expiry is a common implementation. Always release locks safely, ideally with a token check, so a slow node cannot delete another's lock. Bound wait times so a lock outage degrades gracefully.
SET lock:orders:42 <token> NX PX 5000
741. Why is measuring only the application tier misleading when diagnosing latency?
Total latency includes DNS, TLS, load balancers, queueing at the proxy, network hops and database time, none of which appear in application spans unless instrumented. A service can show 20 ms internally while users see 400 ms. Instrument at the edge and propagate trace context so the whole path is visible. Compare server-side and client-side timings to locate the gap.
742. How do you use distributed tracing to find a slow dependency?
Follow the trace waterfall and find the span that dominates the critical path, not just the largest single duration. Repeated sibling spans reveal N+1 behaviour, while gaps between spans indicate queueing or scheduling delays. Compare percentile traces for the same endpoint to distinguish systematic from sporadic issues. Sampling must retain slow traces, so use tail-based sampling.
743. What is tail-based sampling and why does it matter for diagnosing tail latency?
Head-based sampling decides at trace start, so it may discard exactly the slow or failed requests you need. Tail-based sampling buffers a trace and keeps it based on outcome, such as errors or duration above a threshold. This makes p99 investigations feasible without storing everything. It requires more memory and a collector capable of buffering.
744. How do you correlate slow requests with database activity in production?
Emit the trace id as a comment or application name on database commands where supported, and log it alongside command duration. On SQL Server, Query Store and extended events can capture the statement text and duration for the same window. Join on timestamp and query hash to find the offending statement. Without correlation you can only guess which query belongs to which request.
745. What is a reasonable strategy for query timeouts and command cancellation?
Set a command timeout shorter than the caller's timeout so the database call fails fast with a clear error instead of cascading. Always pass cancellation tokens so client disconnects stop work and release connections. Distinguish timeout errors from connection failures in retry policies. Log the statement and parameters on timeout, but never log secrets.
746. How do you avoid unbounded result sets from a query API?
Require pagination with a server-enforced maximum page size and reject or clamp larger requests. Return a continuation token rather than trusting client-supplied offsets. Add a hard cap and a timeout as a backstop. Document limits so clients can design around them.
747. How do you keep an EF Core query stable when the underlying schema evolves?
Query through projections into DTOs so schema changes are absorbed in one mapping place, and keep integration tests that assert result shapes. Avoid SELECT * semantics from raw SQL because added columns change the result set. Review generated SQL when upgrading EF Core versions, since translation changes can alter performance. Pin provider versions and test upgrades deliberately.
748. What changes in query translation should you watch for when upgrading EF Core?
New versions may translate the same LINQ differently, producing different SQL and performance, and some previously client-evaluated expressions may start throwing. Include and split-query defaults have changed between versions, and JSON and primitive collection support expanded. Read the breaking changes and run the query suite against a staging database. Keep regression tests that assert SQL shape for critical queries.
749. How do you decide between a single large service and splitting by bounded context?
Split when teams, deployment cadence or scaling needs genuinely diverge, because each boundary adds operational and consistency cost. Keep a modular monolith when the domain is still being discovered and transactions cross the would-be boundaries frequently. Boundaries should follow business capabilities, not technical layers. Extract services incrementally once the seams are proven.
750. What is the risk of sharing a database between services?
A shared schema becomes a de facto public contract, so any migration is a coordinated release across teams and hidden coupling grows. Services also contend for connections, locks and resources. Prefer one schema or database per service, with data exchange through APIs or events. If sharing is unavoidable, treat the tables as read-only contracts owned by one service.
751. How do you implement idempotency keys for a payment endpoint in ASP.NET Core?
Require the client to send a unique key in a header, persist it together with a hash of the request payload and the resulting response in the same transaction as the business effect, and reject reuse of the same key with a different payload. On a duplicate request, return the stored response instead of re-executing the operation, which makes retries safe. Use a unique index plus optimistic insert to close the race between concurrent retries, and give keys a bounded lifetime.
CREATE UNIQUE INDEX ux_idem ON IdempotencyKeys(TenantId, Key);
752. What is the difference between an idempotency key and a natural idempotency of an operation?
Operations such as "set status to Shipped" are naturally idempotent because repeating them has no additional effect, while "increment balance" or "append order line" are not. Idempotency keys add artificial idempotency to non-idempotent operations by deduplicating client intent. Prefer designing operations that are naturally idempotent and fall back to keys only where the business action cannot be expressed that way.
753. How do you deduplicate messages in a consumer that receives at-least-once deliveries?
Store a processed-message identifier per consumer group in a durable store and check-before-process inside the same transaction that applies the business change. A unique constraint on (consumer, messageId) turns duplicate detection into a single atomic insert, so a crash before commit simply causes reprocessing. Keep the dedup window long enough to cover the broker's maximum redelivery horizon, then purge aggressively to bound storage.
754. Why is the transactional outbox pattern needed when publishing events from a database transaction?
Writing to the database and publishing to a broker are two separate systems, so a crash between them either loses the event or produces an event for a rolled-back change. The outbox writes the event into a table in the same local transaction as the state change, and a relay publishes it afterwards. This yields atomicity without distributed transactions at the cost of a small publication delay.
BEGIN;
UPDATE Orders SET Status = 'Paid' WHERE Id = @id;
INSERT INTO Outbox(Id, Type, Payload) VALUES (@eid, 'OrderPaid', @json);
COMMIT;
755. How do you publish outbox messages reliably and in order?
Run a relay that reads undelivered rows ordered by a monotonic sequence per aggregate, publishes them, and marks them sent; use row locking with SKIP LOCKED so multiple relay instances do not collide. Ordering per aggregate is preserved by publishing sequentially per partition key, not globally. Alternatively use CDC such as Debezium to stream the outbox table, which avoids polling but requires operational maturity.
756. What problem does the inbox pattern solve?
The inbox records inbound messages that have already been handled so that redelivery does not reapply side effects. It is the consumer-side counterpart of the outbox and is usually implemented as an insert into a processed-messages table in the same transaction as the handler's writes. Together, outbox and inbox approximate exactly-once processing over at-least-once transport.
757. Explain the saga pattern and when you would choose orchestration over choreography.
A saga coordinates a long-running business transaction as a sequence of local transactions with compensating actions for failures. Orchestration centralizes the flow in a coordinator, which is easier to reason about, test and observe for complex conditional workflows. Choreography uses events between participants and couples services more loosely but makes end-to-end flow and failure handling harder to trace.
758. What makes a compensating action correct in a saga?
Compensation must be semantically meaningful rather than a literal undo, for example issuing a refund instead of deleting a payment. It must be idempotent and retryable because it can itself fail or be delivered twice, and it must be persisted so that a crashed orchestrator can resume. Some steps are not compensable, such as sending an email, so they should be deferred to the end of the saga.
759. How do you handle a saga step that never completes?
Give each step an explicit timeout and treat expiry as a failure that triggers compensation or a manual intervention state. Record the saga state durably so a recovery worker can resume after process restarts, and expose stuck sagas as an operational metric. Avoid infinite retries that block the whole flow; escalate to a dead-letter or human queue instead.
760. What does eventual consistency mean for the user experience, and how do you design for it?
Reads may briefly return stale data after a write, so the UI must not assume read-your-writes across service boundaries. Design for it with optimistic UI, explicit pending states, polling or push notifications, and by routing read-after-write traffic to the same replica that accepted the write where possible. Also make the convergence time an explicit SLO rather than an accident.
761. How can you provide read-your-writes consistency in an eventually consistent system?
Track a version or watermark per write and have the client pass it on subsequent reads, then wait or redirect until a replica has caught up. Alternatively pin the session to the primary for a short window after a write, or return the authoritative result directly from the write response so the client does not need to read it back.
762. Compare delivery semantics of Kafka, RabbitMQ and Azure Service Bus.
Kafka offers at-least-once by default and effectively-once within its stream-processing primitives via idempotent producers and transactions; consumers commit offsets and can replay. RabbitMQ gives at-least-once with publisher confirms and manual acks, and redelivers unacked messages after channel or connection loss. Azure Service Bus provides at-least-once with peek-lock and complete/abandon, plus duplicate detection and sessions for ordered processing.
763. What is the practical reality of exactly-once delivery?
Exactly-once delivery over an unreliable network is impossible in general, so systems achieve exactly-once effects through idempotent processing or transactional coordination between broker and consumer state. Kafka transactions can make read-process-write atomic when the sink supports them, but once a side effect leaves the transaction boundary, such as an HTTP call, you are back to at-least-once plus deduplication.
764. How does Kafka guarantee ordering, and what breaks it?
Kafka preserves order within a partition, so ordering is only as strong as your partitioning key. Increasing partition count, retries without idempotence enabled, or processing with multiple in-flight requests per partition can reorder records. To preserve per-entity order, key by entity id and ensure max.in.flight.requests.per.connection is 1 or idempotence is on.
765. How do you choose a partition key for an event stream?
Choose a key that matches the ordering and locality requirement, typically the aggregate or tenant id, and verify that the key distribution avoids hot partitions. Avoid keys with skewed cardinality such as country codes for global traffic, and consider composite keys or salting when a single entity is too hot. Changing the key later is a breaking change because it breaks ordering guarantees for existing data.
766. What is a poison message and how do you handle it?
A poison message is one that always fails processing, for example due to malformed payload or a business rule violation, so retrying forever blocks progress. Track a delivery count and after a threshold move the message to a dead-letter queue with failure metadata. Alert on DLQ depth and provide a controlled replay path after the defect is fixed.
767. What should a dead-letter queue strategy include operationally?
DLQs need ownership, alerting on non-zero depth, a retention policy and a documented replay procedure that is idempotent. Store the original payload, headers, exception details and attempt count so triage does not require guesswork. Without a replay path a DLQ becomes a silent data-loss bucket, which defeats its purpose.
768. How do competing consumers interact with ordering?
Competing consumers scale throughput but destroy global ordering because multiple instances process messages concurrently. Ordering can still be preserved by partitioning and assigning each partition to exactly one consumer, as Kafka consumer groups do. On queues without partitions, use sessions or a single consumer per ordered stream.
769. How do you scale consumers without losing per-tenant ordering?
Partition by tenant so all messages for a tenant land on one partition or session, then scale by adding consumers up to the partition count. Beyond that point, increase partitions or split hot tenants into sub-streams with explicit ordering boundaries. Accept that per-tenant throughput is capped by single-partition processing speed.
770. How do you implement a distributed lock with Redis, and what are its limits?
Use SET key token NX PX ttl to acquire and release only if the stored token matches, ideally via a Lua script for atomic compare-and-delete. The fundamental limit is that a lock cannot guarantee safety under pauses longer than the TTL, because the holder may continue working after expiry. Redlock reduces but does not eliminate this; prefer fencing tokens or idempotent operations where correctness matters.
SET lock:order:42 <token> NX PX 10000
771. What is a fencing token and why does it matter for distributed locks?
A fencing token is a monotonically increasing value issued with the lock; every write to the protected resource includes it and the resource rejects tokens lower than the highest seen. This prevents a paused lock holder from committing stale work after its lease expired. Without fencing, a lease only provides mutual exclusion under the assumption that process pauses are bounded.
772. How do you implement leader election with a lease in .NET?
Use a blob lease in Azure Storage or a lease object in Service Bus that must be renewed before expiry, and only the current lease owner performs the leader work. On renewal failure, stop work immediately and let another instance acquire the lease. Combine with fencing tokens or generation numbers for writes that must not be applied by a stale leader.
773. Why is retrying without jitter dangerous?
Simultaneous retries from many clients synchronize into bursts that repeatedly overload a recovering dependency, often called a thundering herd. Full jitter, where the delay is randomized between zero and the exponential cap, spreads retries and dramatically improves recovery. Also cap total attempts and only retry transient, idempotent operations.
var delay = TimeSpan.FromMilliseconds(Random.Shared.Next(0, (int)capMs));
774. How does a circuit breaker differ from retries and timeouts?
A timeout bounds a single call, retries re-attempt a failed call, and a circuit breaker stops calling a failing dependency altogether for a period to allow recovery and fail fast. Used together, the breaker prevents retry storms from consuming threads and connections. Half-open probing with a small number of trial requests determines when to close again.
775. What is the bulkhead pattern in a .NET service?
A bulkhead isolates resources so failure in one dependency or tenant cannot exhaust capacity for others, for example separate connection pools, thread pools or concurrency limiters per downstream. It converts a global outage into a partial degradation. It is often implemented with a rate limiter or a semaphore per dependency.
776. What is request hedging and when is it appropriate?
Hedging issues a second request to another replica if the first is slow beyond a percentile threshold, using whichever responds first and cancelling the other. It reduces tail latency but increases load, so it only makes sense for idempotent reads with spare capacity. Never hedge writes or calls to a dependency already saturated.
777. How do you apply backpressure in a message-driven .NET service?
Limit prefetch and concurrent handlers so the broker does not push more work than the process can absorb, and let unacked messages stay queued. Combine with bounded internal channels that block or shed load when full. Without backpressure the service buffers unboundedly, then fails with memory pressure and loses in-flight work.
778. How does consumer prefetch affect throughput and fairness in RabbitMQ?
A higher prefetch increases throughput by keeping the consumer busy but reduces fairness across consumers and increases memory and redelivery volume on crash. Set prefetch from measured per-message latency and concurrency rather than from a default. Very high prefetch also delays detection of slow consumers because their local buffer hides the problem.
779. What are the core responsibilities of an API gateway?
A gateway terminates TLS, authenticates and authorizes, applies rate limits and quotas, routes and version-translates, and centralizes cross-cutting concerns such as correlation ids and response compression. It should not become a hidden business logic layer, because that creates a distributed monolith and a scaling bottleneck. Keep transformation minimal and push domain policy into services.
780. How do you avoid the API gateway becoming a single point of failure?
Deploy multiple gateway instances across zones, keep the data plane stateless, and use health-checked load balancing with automatic failover. Configure timeouts, circuit breakers and concurrency limits at the gateway so a slow backend cannot exhaust its connections. Treat gateway configuration as code with staged rollouts.
781. Compare client-side and server-side service discovery in .NET.
With client-side discovery the caller queries a registry or uses platform DNS plus a load-balancing policy such as gRPC's round-robin resolver, which removes a network hop and allows fine-grained balancing. With server-side discovery the caller hits a stable virtual address and a load balancer or sidecar chooses the instance, which keeps clients simple. Kubernetes service DNS plus kube-proxy is effectively server-side, while headless services plus a client library give client-side behavior.
782. How does health checking interact with service discovery and load balancing?
A liveness probe answers whether the process should be restarted, while a readiness probe answers whether it should receive traffic, and conflating them causes restart loops or traffic to unready pods. On graceful shutdown, mark the instance not ready first, then drain in-flight requests before exiting. Discovery should stop routing to instances that fail readiness within a bounded detection window.
783. When would you choose gRPC over REST for internal service communication?
gRPC gives strongly typed contracts, compact binary payloads over HTTP/2, streaming and low overhead, which suits high-throughput internal calls and polyglot contracts. It is weaker for browser clients, human-debuggable APIs and public consumption without a proxy. The generated-code workflow also means contract changes require coordinated client regeneration.
784. What are the main trade-offs between REST and GraphQL?
GraphQL lets clients request exactly the fields they need in one round trip, which helps diverse front-ends but complicates caching, rate limiting and query cost control. REST is simpler to cache and secure per endpoint, with clearer semantics for HTTP infrastructure. GraphQL shifts complexity to the server, where depth limits, persisted queries and per-field authorization are essential.
785. What is contract testing and how does it differ from integration testing?
Contract testing verifies that a consumer's expectations and a provider's actual responses agree, without running both together in a full environment. Consumer-driven contracts such as Pact publish expectations that the provider verifies in its own pipeline, catching breaking changes early. Integration tests exercise real collaboration and catch more, but are slower and flakier, so contracts give fast feedback across service boundaries.
786. How do you evolve an event schema without breaking consumers?
Prefer backward-compatible additive changes such as optional fields with defaults, and never repurpose or remove an existing field. Use a schema registry with compatibility checks and a versioned envelope carrying event type and version. When a breaking change is unavoidable, publish a new event type and run both in parallel until consumers migrate.
787. What is the difference between backward and forward compatibility for messages?
Backward compatibility means new readers can read old data; forward compatibility means old readers can read new data, typically by ignoring unknown fields. At-least-once systems usually need both during rolling upgrades, since producers and consumers are never on the same version. Tolerant readers that skip unknown fields and treat missing optional fields as defaults are the practical mechanism.
788. How do you version a public HTTP API in ASP.NET Core?
Choose one strategy such as URL segment, header or media type, apply it consistently, and document deprecation timelines with sunset headers. Add the new version alongside the old, keep both running, and instrument usage per version so you know when it is safe to retire. Avoid breaking changes inside a version even when they seem harmless, such as tightening validation.
789. What are the three pillars of OpenTelemetry and how do they relate?
Traces model a request's path across services as a tree of spans, metrics are aggregated numeric time series, and logs are discrete events. Trace and span ids in logs and exemplars on metrics let you pivot between them. In .NET the OpenTelemetry SDK unifies instrumentation for ASP.NET Core, HttpClient, EF Core and runtime metrics.
790. How does distributed trace context propagation work across service boundaries?
The W3C Trace Context standard carries traceparent and tracestate headers, which ASP.NET Core and HttpClient instrument automatically when propagation is configured. Each hop creates a child span with the same trace id and a new span id, preserving causal relationships. Message brokers require manual propagation by copying the headers into message properties, since there is no HTTP request to carry them.
using var activity = ActivitySource.StartActivity("ProcessOrder");
791. How do you propagate correlation across a message queue?
Inject the trace context and a business correlation id into message headers on publish, then extract and start a consumer span as a child of that context on receive. Without this, each consumer message becomes an orphan root span and the end-to-end flow is invisible. Store the correlation id in logs and in any outbox row so retries and manual replays keep the same identifier.
792. What is structured logging and why does it matter at scale?
Structured logging emits named properties rather than interpolated strings so entries remain machine-queryable and indexable. It enables filtering by tenant, order id or trace id instead of fragile text search. Message templates should stay constant with varying parameters, which also keeps cardinality and aggregation predictable.
logger.LogInformation("Order {OrderId} shipped to {Region}", id, region);
793. How do you control logging cost in a high-volume .NET service?
Set per-category levels, sample verbose paths such as health checks and successful reads, and never log full payloads by default. Route high-volume diagnostic logs to cheap storage with short retention and reserve indexed storage for errors and audit events. Monitor ingestion volume as an SLO-adjacent metric, because log cost often grows faster than traffic.
794. How do you avoid high-cardinality metrics that break your monitoring backend?
Never use unbounded values such as user id, order id or raw URL as metric dimensions; aggregate those into bounded buckets or keep them in traces and logs instead. Keep label sets small and reviewed, since each unique combination is a separate time series. Cardinality explosions are a common cause of monitoring outages during incidents, exactly when you need it most.
795. What is the difference between an SLI, an SLO and an error budget?
An SLI is a measured ratio such as successful requests over total requests; an SLO is the target for that SLI over a window, for example 99.9 percent over 30 days. The error budget is the complement, the allowed failure volume, and it governs release velocity. When the budget is exhausted, teams freeze risky changes and prioritize reliability work.
796. Why should alerts fire on symptoms rather than causes?
Users experience symptoms such as errors or latency, while causes like high CPU may be normal or harmless, which produces alert fatigue. Alert on SLO burn rate so pages correspond to real user impact and are actionable. Keep cause-based signals as dashboards or tickets for diagnosis rather than paging triggers.
797. What is multi-window multi-burn-rate alerting?
It combines a short window to detect fast burns with a long window to confirm significance, reducing both false positives and slow detection. For example page when a 1-hour burn rate exceeds 14x and a 5-minute window confirms it is still ongoing. This approach ties alert urgency directly to how much error budget is being consumed.
798. What is chaos engineering and what safeguards make it responsible?
Chaos engineering deliberately injects failures such as latency, instance kills or dependency errors to validate resilience assumptions before they fail in production. Responsible practice starts in staging with a hypothesis, a bounded blast radius, an abort switch and stakeholder awareness. Measure the outcome against an SLI so experiments produce knowledge rather than just incidents.
799. How would you test that a circuit breaker actually fails fast?
Inject a dependency that fails a configurable number of times, drive enough calls to open the breaker, then assert that subsequent calls return the fallback or throw immediately without invoking the dependency. Measure elapsed time and the dependency's invocation count, not just the exception type. Also verify the half-open transition after the break duration by allowing one probe to succeed and confirming the breaker closes.
800. Compare the unit, integration and end-to-end levels of the test pyramid.
Unit tests exercise a single component in isolation with fast, deterministic execution and form the broad base. Integration tests cross real boundaries such as a database or broker and catch wiring and serialization defects. End-to-end tests validate critical user journeys through the whole system and should be few because they are slow and brittle.
801. Why is the test pyramid often replaced by a testing trophy in service architectures?
In distributed systems much of the risk lives at boundaries, so integration tests deliver more value per test than pure unit tests of trivial logic. The trophy shape keeps a solid base of unit tests but emphasizes integration testing and a thin E2E layer. The principle is to invest where defects actually occur rather than to hit a ratio.
802. What factors should drive your choice between xUnit, NUnit and MSTest?
All three are capable; xUnit is the common default in modern .NET with strong parallelization and fixture-based dependency injection, NUnit offers richer parameterization and attributes, and MSTest integrates tightly with Microsoft tooling. Choose based on team familiarity and ecosystem rather than raw features, and stay consistent inside a solution. Migration cost between them is rarely worth it without a concrete unmet need.
803. How does xUnit handle test isolation and shared context?
xUnit creates a new instance of the test class per test, so instance state is naturally isolated. Shared setup is expressed through IClassFixture for per-class state or ICollectionFixture for state shared across a collection, with collections also controlling parallelization. This differs from NUnit's set-up-once model, which requires more discipline to keep tests independent.
804. When should you not mock a dependency?
Do not mock types you do not own, value objects, or anything whose real behavior is cheap and deterministic, because mocks encode your assumptions rather than reality. Avoid mocking to test interactions that are not part of the contract, which produces tests that break on refactoring. Prefer real implementations or in-memory fakes for infrastructure, and reserve mocks for external, slow or non-deterministic collaborators.
805. What are the pitfalls of mocking with Moq or NSubstitute?
Over-specified interaction tests break when implementation details change, and loose mocks silently return default values, hiding missing setup. Mocking virtual or interface members only means sealed or static dependencies force awkward wrappers. Prefer asserting observable outcomes and use strict behavior only where a call is genuinely required.
var repo = Substitute.For<IOrderRepository>();
repo.GetAsync(42).Returns(order);
806. How does WebApplicationFactory improve integration testing in ASP.NET Core?
It boots the real application pipeline in-process with TestServer, so routing, filters, model binding, serialization and DI are exercised without network overhead. You can override registrations in ConfigureTestServices to substitute infrastructure. This catches defects that controller-level unit tests miss, such as missing DI registrations or incorrect middleware order.
var client = factory.WithWebHostBuilder(b =>
b.ConfigureTestServices(s => s.AddSingleton<IClock, FakeClock>())).CreateClient();
807. What are the risks of testing against in-memory providers instead of real infrastructure?
EF Core's in-memory or SQLite provider does not enforce relational constraints, transaction semantics, concurrency tokens or provider-specific SQL, so tests pass where production fails. It also encourages lazy-loading and query patterns that behave differently on a real server. Use a real database via Testcontainers for anything touching persistence behavior.
808. How does Testcontainers change your integration testing strategy?
Testcontainers starts real Dockerized dependencies such as SQL Server, PostgreSQL, Redis or Kafka per test run, giving production-faithful behavior with disposable state. It removes the shared-environment problem where tests interfere with each other. The cost is Docker availability in CI and slower startup, mitigated with collection fixtures and reusable containers.
await new ContainerBuilder().WithImage("postgres:16").WithPortBinding(5432, true).StartAsync();
809. How do you manage database schema and migrations in automated tests?
Apply the same migrations used in production rather than EnsureCreated, so migration defects surface in CI. Reset state between tests with transaction rollback where possible, or recreate the schema per collection when isolation demands it. Keep seed data minimal and deterministic, and never depend on data left by another test.
810. How do you diagnose and eliminate flaky tests?
Treat flakiness as a defect: capture the seed, timings, logs and machine state, and quarantine the test so it stops eroding trust. Common causes are time and timezone dependencies, unawaited tasks, shared mutable state, port collisions and reliance on ordering. Inject IClock, avoid Thread.Sleep in favor of polling with timeouts, and isolate external resources per test.
811. What does mutation testing tell you that code coverage does not?
Coverage measures execution, while mutation testing modifies the code, such as flipping a comparison, and checks whether a test fails. Surviving mutants reveal assertions that are missing or too weak even on fully covered lines. It is computationally expensive, so run it on critical assemblies or incrementally rather than across the whole solution.
812. How do you design a meaningful performance test for a .NET API?
Define the question first, such as capacity at a latency SLO, then model a realistic workload mix with think time and payload sizes. Warm up the JIT and connection pools, run against production-like infrastructure, and measure p95 and p99 rather than averages. Report saturation signals like CPU, GC pauses and queue depth alongside throughput so results are explainable.
813. What is broken object level authorization and why is it the top API risk?
BOLA occurs when an endpoint accepts an object identifier and returns it without verifying that the caller owns or may access that object. It tops the OWASP API Security Top 10 because it is easy to introduce, hard to detect with scanners, and exposes other tenants' data directly. Mitigate with per-request authorization that binds the object to the authenticated subject and tenant, enforced in the data layer rather than the UI.
var order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id && o.TenantId == tenantId);
814. How do you defend against mass assignment in ASP.NET Core?
Never bind request payloads directly to persistence entities, because clients can then set fields such as IsAdmin or Balance. Use explicit request DTOs containing only client-settable properties and map them to domain objects deliberately. The [Bind] attribute and BindNever are weaker and easy to forget, so separate models are the reliable approach.
815. What is broken function level authorization?
It is the failure to restrict administrative or privileged operations to the roles that should perform them, often because authorization is applied at the controller level but not per action. Enumerate every endpoint and require an explicit policy rather than relying on default authenticated access. Test with low-privilege tokens as part of your security regression suite.
816. How do you prevent excessive data exposure in API responses?
Return purpose-built response DTOs instead of serializing domain entities, so new internal fields never leak automatically. Apply allow-lists in serialization and review payloads for sensitive fields such as internal ids, emails or tokens. The client filtering data it does not need does not protect anything, since the raw response is visible on the wire.
817. What is unrestricted resource consumption and how do you mitigate it?
Attackers can exhaust CPU, memory or downstream quotas by requesting expensive operations repeatedly or with pathological inputs. Mitigate with request size limits, pagination caps, query complexity limits, per-client rate limits and timeouts on expensive work. Also bound concurrency per endpoint so a single client cannot monopolize the service.
818. When is the client credentials flow appropriate in OAuth 2.0?
It is for machine-to-machine calls where no user is involved and the client can keep a secret confidential, such as a background service calling an API. The token represents the application itself, so scopes should be minimal and the secret stored in a vault rather than configuration files. It must never be used from a browser or mobile app, where the secret cannot be protected.
819. How does authorization code flow with PKCE protect public clients?
The client generates a random code verifier, sends its hash as code_challenge, and must present the original verifier when redeeming the code. An attacker who intercepts the authorization code on a mobile device cannot redeem it without the verifier. PKCE also protects confidential clients against code injection, so it is recommended for all authorization code flows.
challenge = Base64Url(SHA256(verifier))
820. Why is the implicit flow deprecated?
The implicit flow returns access tokens directly in the URL fragment without a back-channel exchange, exposing them to browser history, logs and referrer leakage with no client authentication. Authorization code with PKCE plus a token endpoint exchange now works in browsers without a client secret. Use the current best practice, which also enables refresh tokens and sender-constrained tokens.
821. How should access token and refresh token lifetimes be chosen?
Keep access tokens short-lived, commonly five to fifteen minutes, to bound the damage of theft, and use refresh tokens with longer lifetimes plus rotation and reuse detection. Sliding sessions with absolute maximums balance user convenience against risk. Never treat a revoked user as logged out until the access token expires unless you add introspection or a revocation check.
822. What is refresh token rotation and why does reuse detection matter?
Each refresh returns a new refresh token and invalidates the previous one, so a stolen token becomes useless once the legitimate client refreshes. If an already-used token is presented, the authorization server assumes compromise and revokes the whole token family. This converts silent long-term access into a detectable security event.
823. What is the difference between ID token and access token?
The ID token is a signed assertion of authentication for the client, describing who the user is and when they authenticated, and must not be sent to APIs as authorization. The access token authorizes API calls and is intended for the resource server, which validates it independently. Mixing them up leads to confused-deputy vulnerabilities and broken authorization.
824. Which JWT validation checks must a .NET API perform?
Validate the signature against a trusted key set, the issuer, the audience, the lifetime with a small clock skew, and the token type or algorithm allow-list. Set RequireSignedTokens and RequireExpirationTime, and disable all claim-type mapping surprises explicitly. Skipping audience validation allows a token issued for another API to be replayed against yours.
options.TokenValidationParameters.ValidAudience = "orders-api";
options.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(30);
825. Why is accepting the alg header at face value dangerous?
If the validator trusts the token's alg field, an attacker can present an unsigned token with alg none or switch from RS256 to HS256 and sign with the public key as an HMAC secret. Always configure an explicit algorithm allow-list on the validation parameters so the header cannot select the verification method. The vulnerability is a validator configuration flaw, not a JWT format flaw.
826. How should the kid header be used during validation?
kid selects which key from the issuer's JWKS should verify the signature, which enables rotation without invalidating live tokens. The validator must only consider keys fetched from the trusted metadata endpoint, never a key embedded in the token. If kid is missing or unknown, fail closed and refresh the key set once before rejecting.
827. How do you handle signing key rotation and JWKS caching?
Fetch JWKS over HTTPS from the issuer's metadata, cache it with a modest TTL, and refresh on an unknown kid with rate limiting to avoid a fetch storm. Publish new keys before signing with them and keep old keys for at least the maximum token lifetime. In .NET, configuration managers handle this automatically when Authority metadata is used.
828. How do you choose between Azure Key Vault, user secrets and environment variables?
User secrets are for local development only and are never a deployment mechanism. Environment variables are convenient for containers but visible to process inspection and often end up in logs or crash dumps. Key Vault or an equivalent vault provides access control, auditing and rotation, and should be reached through managed identity rather than stored credentials.
829. How does managed identity remove secrets from a .NET application?
The platform injects a federated identity token to the workload, and the app authenticates to Key Vault or storage without any stored credential. DefaultAzureCredential chains local developer credentials with the deployed identity so the same code works in both environments. This eliminates the rotation and leakage problem for the credentials that would otherwise protect everything else.
builder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential());
830. Why should secrets never be placed in appsettings.json or source control?
Configuration files are copied to build artifacts, container images and log outputs, and git history makes removal effectively impossible. A leaked secret must be treated as compromised and rotated, not just deleted. Use vault references so configuration holds only a pointer, and enable secret scanning with push protection in the repository.
831. What is the difference between encryption at rest and in transit, and what does each protect against?
Encryption in transit protects data on the network from interception and tampering, typically TLS with strong cipher suites and certificate validation. Encryption at rest protects stored data from disk or backup theft, but not from a compromised application that has the keys. Neither protects against an attacker who can query the API legitimately, which is why authorization and auditing remain necessary.
832. How does the ASP.NET Core Data Protection API work and what must you configure in a farm?
Data Protection encrypts payloads such as cookies and antiforgery tokens with a key ring, and by default keys are stored locally and protected with DPAPI or the machine key. In a multi-instance deployment, all instances must share the same key ring and a stable application name, otherwise cookies issued by one node fail on another. Persist keys to a shared store and protect them with a certificate or vault.
services.AddDataProtection().PersistKeysToAzureBlobStorage(uri).ProtectKeysWithAzureKeyVault(keyId, cred);
833. Why is input validation alone insufficient as a security control?
Validation reduces the attack surface but cannot anticipate every dangerous context, so it must be combined with encoding, parameterization and authorization. Allow-list validation with length, range and format constraints is far more robust than block-listing dangerous characters. Validate on the server always, and treat client-side validation as a usability feature only.
834. How do you prevent SQL injection in a .NET application?
Use parameterized queries or an ORM with parameter binding and never concatenate user input into SQL text. Dynamic identifiers such as sort columns cannot be parameterized, so map them through a strict allow-list. Stored procedures do not help if they build SQL dynamically inside.
await db.Database.ExecuteSqlInterpolatedAsync($"UPDATE Orders SET Status = {status} WHERE Id = {id}");
835. What is the difference between HTML encoding, attribute encoding and JavaScript encoding?
Each output context has different escaping rules, and using the wrong one still allows injection. Razor encodes by default for HTML text and attributes, but data placed inside script blocks or URLs needs context-specific encoding. Prefer avoiding inline script with user data entirely, and use JSON serialization APIs when embedding values in JavaScript.
836. How do you protect an ASP.NET Core API against CSRF?
Cookie-authenticated endpoints need antiforgery protection because browsers attach cookies automatically to cross-site requests. Use the antiforgery token plus SameSite cookies, and require a custom header for state-changing calls from JavaScript clients. Purely bearer-token APIs are not vulnerable in the same way, because the browser does not attach the token automatically.
837. What are the risks of a permissive CORS policy?
Allowing any origin with credentials lets a malicious site read authenticated responses on a victim's behalf, effectively bypassing the same-origin policy. Specify an explicit origin allow-list, never combine wildcard origins with AllowCredentials, and restrict methods and headers. Remember that CORS is a browser control and does not protect the API from non-browser clients.
838. What is server-side request forgery and how do you prevent it in .NET?
SSRF tricks the server into making requests to attacker-chosen destinations, reaching internal metadata endpoints or services behind the firewall. Prevent it by allow-listing hosts and schemes, resolving and validating the IP after DNS resolution, and blocking link-local, loopback and private ranges. Follow redirects only within the allow-list and consider egress proxies for any user-supplied URL fetch.
839. Why must XML parsers be configured securely against XXE?
External entity expansion lets an attacker read local files, perform SSRF or cause denial of service through entity blowup. Disable DTD processing and external entities explicitly, since defaults differ across parser versions. Use XmlReaderSettings with DtdProcessing set to Prohibit and avoid XmlDocument with default settings.
new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }
840. What makes insecure deserialization so dangerous?
Deserializers that can instantiate arbitrary types or invoke setters during construction give an attacker control over object graphs, which can lead to remote code execution or authorization bypass. Avoid BinaryFormatter and other type-driven serializers entirely, and use System.Text.Json with explicit DTOs. If polymorphic payloads are required, use a strict type allow-list rather than the type name in the payload.
841. How do you secure a file upload endpoint?
Enforce size and count limits, validate content by inspecting magic bytes rather than trusting the extension or content type, and generate a new server-side filename. Store uploads outside the web root or in object storage with no execute permission, and serve them through a handler that sets a safe content type and Content-Disposition. Scan for malware asynchronously and re-encode images to strip embedded payloads.
842. How do you implement rate limiting in ASP.NET Core?
Use the built-in rate limiter middleware with fixed window, sliding window, token bucket or concurrency limiters per endpoint and per client identity. Partition by API key or user id rather than IP alone, because IP is shared behind NAT and spoofable in some paths. Return 429 with Retry-After and apply limits at the gateway too so direct-to-service traffic cannot bypass them.
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("api", w => { w.PermitLimit = 100; w.Window = TimeSpan.FromMinutes(1); }));
843. What should an audit log contain, and how does it differ from application logs?
Audit logs record who did what to which resource, when, from where, and with what outcome, and they must be tamper-evident and retained per policy. Application logs are diagnostic and sampled, while audit records are complete for the audited actions and must never be dropped. Write them append-only to a separate sink and treat them as sensitive because they often contain personal data.
844. How do you minimize PII in logs and telemetry?
Log identifiers rather than payloads, hash or mask sensitive fields at the logging boundary, and review telemetry pipelines for accidental capture of bodies, query strings and headers. Redact authorization headers and cookies before enrichment, and never log full tokens or card data. Apply the same retention and access controls to logs as to databases, since they often escape data-protection review.
845. How do you enforce multi-tenant data isolation in EF Core?
Carry the tenant id in the authenticated principal, never in the request body, and apply a global query filter keyed to that value so reads cannot cross tenants. Populate TenantId on insert in SaveChanges and add a composite index and foreign keys including it. For higher assurance, use database-level row security or separate databases per tenant.
modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == _tenant.TenantId);
846. What are the weaknesses of application-level tenant filters?
They can be bypassed with IgnoreQueryFilters, raw SQL, or code paths that do not go through the filtered context, and they silently fail open if the tenant claim is missing. Add a strict check that throws when no tenant is resolved, cover queries with tests, and supplement with database row-level security for defense in depth.
847. How do you implement a GDPR erasure request in a distributed .NET system?
Maintain a data map of which services and stores hold personal data, then fan out a deletion or anonymization command that is idempotent and tracked to completion. Distinguish hard deletion from legal retention obligations, and handle backups and derived stores such as search indexes and analytics with documented timelines. Record the request, the scope and the completion evidence for accountability.
848. How do you reconcile deletion with event-sourced or immutable logs?
You cannot rewrite an append-only log cheaply, so use crypto-shredding where the payload is encrypted per subject and deleting the key renders it unreadable. Alternatively tombstone the subject id and keep only non-personal aggregates. Design this before launch, because retrofitting erasure into immutable storage is extremely expensive.
849. How does the CAP theorem guide real architecture decisions in .NET systems?
During a network partition you must choose between consistency and availability, and since partitions are inevitable the real decision is which operations sacrifice availability. Most systems are not purely CP or AP but mix per-operation choices, for example strong consistency for payments and availability for the product catalog. State the choice explicitly per data set rather than claiming the system is CA.
850. Why is two-phase commit rarely used across microservices?
Two-phase commit requires all participants to hold locks from prepare until commit, so a single slow or failed participant blocks everyone and can leave resources locked indefinitely. It also requires compatible transaction managers across heterogeneous stores and brokers, which is operationally fragile. Sagas with compensation achieve business consistency without distributed locking, at the cost of accepting intermediate states.
851. What happens during a Kafka consumer group rebalance and how do you reduce its impact?
Partitions are reassigned among consumers, and during a stop-the-world rebalance all consumption pauses, with duplicates likely for records processed but not yet committed. Cooperative sticky assignment rebalances incrementally instead of revoking everything, and static membership avoids rebalances on rolling restarts. Keep poll intervals short relative to max.poll.interval.ms and commit after processing.
852. When should you commit Kafka offsets, and what are the consequences of each choice?
Committing after processing gives at-least-once semantics with possible duplicates on crash, while committing before processing risks message loss. Async commits improve throughput but can reorder or drop commits on failure, so use them with idempotent processing and monotonic offset tracking. Store offsets together with derived state when you need atomic read-process-write.
853. How do Azure Service Bus sessions provide ordered processing?
Sessions bind messages with the same SessionId to a single consumer at a time, preserving order and enabling per-entity state on the receiver. This gives ordered, exclusive processing per session while still allowing many sessions to be processed in parallel. It is the natural fit for per-tenant or per-order workflows where global ordering is unnecessary.
var session = await client.AcceptNextSessionAsync(queueName);
854. How does Service Bus duplicate detection differ from application-level idempotency?
Duplicate detection suppresses messages with the same MessageId inside a configured time window, which handles short-term producer retries. It does not protect against duplicates arriving after the window or against reprocessing after a successful send followed by a crash. Application-level deduplication in the handler remains necessary for correctness.
855. Why is PUT considered idempotent while POST is not?
PUT replaces the resource at a client-known URI, so repeating it produces the same state, whereas POST asks the server to create or process something and may create a new resource each time. This is a semantic contract that infrastructure and clients rely on for safe retries. If you need retryable creation with POST, use an idempotency key and return the original result.
856. What are the trade-offs between polling an outbox table and using CDC?
Polling is simple, portable and easy to reason about, but adds latency and database load and needs careful locking to avoid double publication. CDC such as Debezium reads the transaction log with low latency and no query load, but couples you to database-specific tooling and operational complexity. Many teams start with polling and move to CDC when latency or load becomes a constraint.
857. How do you safely replay messages from a dead-letter queue?
Fix the root cause first, then replay through a controlled tool that re-injects with the original payload and headers plus a replay marker, ideally into a staging queue or with a low rate limit. Ensure handlers are idempotent so partial prior success does not duplicate effects. Track replayed messages separately so a second failure does not silently loop.
858. Compare head-based and tail-based trace sampling.
Head-based sampling decides at the start of a trace, which is cheap and consistent across services but cannot know whether the request will fail or be slow. Tail-based sampling buffers spans and decides after seeing the whole trace, so it can retain all errors and outliers at a fraction of the volume. Tail sampling requires a collector topology that sees all spans for a trace, typically routed by trace id.
859. What are RED and USE metrics and when do you use each?
RED describes request-driven services through rate, errors and duration, which maps naturally to SLOs. USE describes resources through utilization, saturation and errors, which is better for diagnosing hosts, queues and connection pools. Use RED for the service view and USE for the underlying resource view so you can see both symptom and cause.
860. Why are histograms better than averages for latency monitoring?
Averages hide the tail, and latency distributions are skewed, so a good mean can coexist with a terrible p99. Histograms preserve the distribution and allow percentile aggregation across instances, which averages of averages cannot do. Percentiles also cannot be averaged after the fact, so record histograms rather than precomputed percentiles per instance.
861. How do OpenTelemetry resource attributes and semantic conventions help?
Resource attributes describe the emitting entity, such as service name, version and deployment environment, and semantic conventions standardize attribute names across libraries. This makes traces and metrics queryable and comparable across services and vendors without custom mapping. Set service.name and service.version deliberately, since they drive dashboards and deployment correlation.
builder.Services.ConfigureOpenTelemetryMeterProvider(b =>
b.ConfigureResource(r => r.AddService("orders-api", serviceVersion: "1.4.2")));
862. What is the difference between health checks and synthetic monitoring?
Health checks run inside the process and report dependency and readiness state for orchestration decisions, while synthetic monitors exercise the system from outside as a user would. Health checks can pass while real traffic fails, for example when routing or DNS is broken. Use both, and keep liveness checks shallow so they do not restart healthy pods during a dependency outage.
863. Distinguish load, stress and soak testing.
Load testing validates behavior at expected peak traffic against latency and error SLOs. Stress testing pushes beyond capacity to find the breaking point and confirm graceful degradation rather than collapse. Soak testing runs at sustained moderate load for hours to expose leaks, fragmentation, growing queues and connection exhaustion.
864. How do approval or golden-file tests help protect wire contracts?
They serialize a representative object graph and compare it against a reviewed snapshot, so any change to field names, casing, nullability or ordering appears as a diff in review. This catches accidental breaking changes that compile fine and pass behavioral tests. Keep snapshots small and commit them as reviewed artifacts.
865. How do SAST, DAST and dependency scanning complement each other?
SAST analyzes source for dangerous patterns early, DAST probes the running application for exploitable behavior, and dependency scanning flags vulnerable or unmaintained packages. None is sufficient alone, and all produce noise that needs triage and ownership. Add an SBOM so you can answer quickly which services ship a newly disclosed component.
dotnet list package --vulnerable --include-transitive
866. When would you use mTLS instead of OAuth for service-to-service authentication?
mTLS authenticates workloads by certificate, which suits a service mesh or zero-trust network where identity is bound to the workload rather than the request. OAuth carries user or client context and fine-grained scopes that mTLS cannot express. Many systems use both: mTLS for transport-level workload identity and tokens for authorization decisions.
867. What problem do sender-constrained tokens such as DPoP solve?
Bearer tokens are usable by anyone who steals them, so DPoP binds the token to a client-held key by requiring a proof-of-possession JWT on each request. A stolen access token is then useless without the private key. This reduces the impact of token leakage from logs, proxies or compromised storage.
868. What is the on-behalf-of flow and when is it required?
An API that received a user token exchanges it for a downstream token representing the same user, so the next service sees the original identity rather than the calling service's identity. It is required when downstream authorization must reflect the end user, and it preserves scopes and consent boundaries. Without it, services either impersonate the user with excessive permissions or lose the user context entirely.
869. Compare API keys and OAuth client credentials for partner integrations.
API keys are simple to issue and identify a caller but carry no scopes, expiry or standard rotation story, and they are often pasted into insecure places. Client credentials give scoped, expiring tokens with auditable issuance and revocation. Prefer OAuth for anything sensitive, and if keys are required, scope and rotate them and never treat them as user identity.
870. How do policy-based and resource-based authorization differ in ASP.NET Core?
Policy-based authorization evaluates requirements against the user and optional resource up front, which suits coarse rules such as role plus tenant membership. Resource-based authorization calls IAuthorizationService against the loaded entity, which is necessary when the decision depends on the resource's state, such as ownership or draft status. Use resource-based checks for object-level access to avoid BOLA.
var result = await authorizationService.AuthorizeAsync(User, order, "OrderOwner");
871. How do you rotate application secrets with zero downtime?
Support overlapping validity so the old and new secret both work during a transition window, deploy consumers with both configured, switch producers to the new secret, then retire the old one. Automate rotation in the vault and reload configuration without a restart where supported. Monitor authentication failures during the window so a missed consumer is detected immediately.
872. What is envelope encryption and why is it used?
Data is encrypted with a per-object data key, and that key is itself encrypted by a long-lived master key in a vault or HSM. This limits how much data one key protects, avoids sending bulk data to the KMS, and makes rotation cheap because only key wrappings change. It also enables crypto-shredding by destroying a single data key.
873. Distinguish pseudonymization from anonymization.
Pseudonymization replaces identifiers with tokens that can be reversed with additional data, so the data remains personal under GDPR but lowers risk. Anonymization must be irreversible and prevent re-identification, at which point the data falls outside personal-data rules. Hashing an email without a secret salt is pseudonymization at best, since it is reversible by enumeration.
874. How do you implement a data subject access request efficiently?
Build a per-service export endpoint that collects data for a subject from each store, including logs and object storage where feasible, and returns a machine-readable bundle. Verify the requester's identity before releasing anything, and apply the statutory response window from receipt. Automate discovery with a maintained data map so the request does not depend on tribal knowledge.
875. How do you enforce data retention automatically?
Classify data by retention category at write time, then delete or archive with scheduled jobs that are idempotent and observable. Use storage-native lifecycle policies for blobs and TTL indexes for documents, and include backups and derived copies in the policy. Track deletion counts and failures, since silent retention failures are a compliance risk.
876. How do you prevent a noisy tenant from degrading service for others?
Apply per-tenant quotas at the gateway and per-tenant concurrency or rate limits inside the service, and isolate expensive workloads into separate queues or worker pools. Track per-tenant latency and error metrics so skew is visible rather than hidden in aggregates. For the largest tenants, consider dedicated infrastructure or sharding by tenant tier.
877. What is event upcasting and why is it preferable to rewriting history?
Upcasting transforms older event versions into the current shape when they are read, so consumers stay simple while stored events remain immutable. Rewriting history requires migrating the log and risks corrupting the audit trail. Keep upcasters small, versioned and covered by tests, and add a new upcaster rather than editing an existing one.
878. How do schema registry compatibility modes constrain event evolution?
Modes such as backward, forward, full and transitive define which changes the registry will accept, for example adding an optional field is backward compatible but removing one is not. Enforcing a mode in CI prevents a producer from publishing a schema that breaks consumers. Choose the strictest mode your consumers can tolerate, and design changes to fit it.
879. What is deadline propagation and why does it matter in a call chain?
Each request carries a remaining-time budget that shrinks at every hop, so downstream calls fail fast instead of working on a request the caller has abandoned. gRPC propagates deadlines natively, while HTTP requires a header or a custom convention. Without it, timeouts multiply along the chain and resources stay busy for work nobody is waiting for.
var deadline = DateTime.UtcNow.AddMilliseconds(remaining);
880. How do canary releases and feature flags reduce the risk of a distributed deployment?
A canary routes a small share of traffic to the new version and compares SLOs before a full rollout, while a feature flag decouples deployment from activation and allows instant rollback without redeploying. Together they limit blast radius and shorten time to mitigation. Instrument both by version and flag state so a regression is attributable, and remove stale flags to avoid combinatorial debt.
881. What is the difference between .NET LTS and STS releases, and how does that affect your upgrade planning?
LTS releases ship every two years and are supported for three years; STS releases ship in the intermediate year and are supported for eighteen months. LTS is the default target for production workloads because it reduces forced migrations, while STS is appropriate when you need newer runtime features or want to stay close to the platform's direction. A pragmatic plan keeps production on LTS and validates the next LTS on a non-critical service during the STS cycle.
882. How would you describe the .NET release cadence introduced with .NET 5 and later?
Microsoft ships a new major .NET version every November, alternating between LTS and STS, and patches are delivered monthly. Applications should be re-targeted at least once a year to stay inside the support window. Teams that skip multiple versions accumulate migration debt and lose security patches once the runtime goes out of support.
883. What do collection expressions add to C# 12, and where do they fall short?
Collection expressions provide a unified, target-typed syntax using square brackets for arrays, spans, and any type with a collection builder. Spread elements let you inline an existing sequence, which removes a lot of boilerplate concatenation code. They do not replace LINQ and can allocate when the target type has no efficient builder, so hot paths still need measurement.
int[] a = [1, 2, 3];
List<string> names = ["ana", "bo"];
Span<int> all = [..a, 4, 5];
884. How do primary constructors for classes differ from records, and what is the common misuse?
Primary constructor parameters on a class are captured only if used, and they do not become public properties automatically, unlike positional records. The common misuse is capturing a mutable dependency in many members so the compiler generates a hidden field that outlives the intended scope. Prefer explicit fields or assignment when the value is used across the type.
885. What problem do required members solve, and what are their runtime limitations?
The required modifier forces object initializers to set a property, which makes immutable-style initialization safe without constructor overloads. Enforcement is compile-time only, so reflection, serializers, and Activator.CreateInstance can bypass it unless the type is annotated or validated. Types with required members also need SetsRequiredMembers on constructors that set them to avoid false errors.
public sealed class Options
{
public required string Endpoint { get; init; }
public int Retries { get; init; } = 3;
}
886. When are raw string literals preferable to verbatim strings?
Raw string literals start and end with three or more quotes, need no escaping for quotes, backslashes, or braces, and strip leading indentation based on the closing delimiter. They are the cleanest option for JSON, SQL, and regex payloads. Verbatim strings are still fine for simple paths but cannot express embedded quotes without doubling.
var json = """
{ "id": 1, "path": "C:\\temp" }
""";
887. What does the alias-any-type feature enable in C# 12?
Using aliases can now target tuples, arrays, pointers, and other non-named types, and generic aliases can be defined with the using directive. This lets a team give semantic names to structural types, which improves readability in domain-heavy code. Aliases are file or global scoped and disappear at compile time, so they do not affect the public API surface.
using Point = (int X, int Y);
using StringMap = System.Collections.Generic.Dictionary<string, string>;
888. How do default lambda parameters change the way you write small handler code?
Lambda expressions can declare default parameter values, so a delegate can be invoked with fewer arguments. This is convenient for expression-tree-free code such as local handlers or minimal API filters, but the default is baked into the delegate signature rather than being optional metadata. Callers must still use the delegate type, not reflection, to get the default behaviour.
var greet = (string name = "guest") => $"hi {name}";
889. When is keyed dependency injection the right choice, and what are its pitfalls?
Keyed services let you register multiple implementations of one interface and resolve them by key, which removes factory switches for things like multiple payment providers. The pitfall is hiding strategy selection inside the container, which makes dependencies implicit and harder to test. Prefer keys at composition boundaries and keep the selection logic explicit where the key is known.
builder.Services.AddKeyedSingleton<IPayment, StripePayment>("stripe");
app.MapPost("/pay", ([FromKeyedServices("stripe")] IPayment p) => p.Charge());
890. Why should production code depend on TimeProvider instead of DateTime.UtcNow?
TimeProvider abstracts the clock and timers, so tests can use FakeTimeProvider and advance time deterministically instead of sleeping. It also centralizes time zone and timestamp semantics, which matters for auditing and scheduling logic. The main discipline is injecting it once and never mixing it with static time calls in the same component.
public sealed class Expiry(TimeProvider clock)
{
public bool IsExpired(DateTimeOffset at) => at < clock.GetUtcNow();
}
891. What do frozen collections give you compared with a regular dictionary?
Frozen collections are optimized for read-heavy, build-once workloads: creation is more expensive, but lookups are faster and thread-safe with lower memory overhead. They suit static lookup tables such as status codes or allow-lists created at startup. Do not use them for data that changes, since every mutation means rebuilding the frozen instance.
private static readonly FrozenDictionary<int, string> Codes =
new Dictionary<int, string> { [200] = "OK", [404] = "NotFound" }.ToFrozenDictionary();
892. How does SearchValues improve high-throughput string scanning?
SearchValues precomputes an optimized matcher for a fixed set of characters or strings, allowing the runtime to use vectorized paths in IndexOfAny and similar APIs. It is created once and cached, which avoids rebuilding per-call search state. The win is largest on hot parsing loops such as tokenizers or log scrubbers.
private static readonly SearchValues<char> Separators = SearchValues.Create(",;|");
int i = span.IndexOfAny(Separators);
893. What are the trade-offs of using System.Text.Json source generation?
Source generation removes reflection at runtime, which improves startup, reduces allocations, and is required for Native AOT trimming scenarios. The cost is extra generated code, slower builds, and constraints on polymorphic or dynamic shapes that the generator cannot model. It is usually worth enabling for DTO-heavy services and mandatory for AOT-published APIs.
[JsonSerializable(typeof(OrderDto))]
internal partial class ApiJsonContext : JsonSerializerContext { }
894. What do you lose when you enable Native AOT for an ASP.NET Core service?
Native AOT gives fast startup, small memory footprint, and no JIT, which is ideal for scale-to-zero and CLI tools. In exchange you lose dynamic assembly loading, most reflection-based serialization, runtime code generation, and some libraries that depend on them. Profiling and debugging are also less familiar, so AOT is a deliberate architectural choice rather than a checkbox.
895. How do trimming and AOT differ in risk profile?
Trimming removes unreachable IL but still runs on the JIT, so failures appear as missing members at runtime. AOT goes further and compiles ahead of time, so unsupported patterns fail at publish time or require annotations. Trimming is easier to adopt incrementally, while AOT demands a dependency audit up front.
896. Which minimal API improvements matter most for a production service?
Route groups, typed results, endpoint filters, and validation integration remove most reasons to fall back to controllers for small services. TypedResults gives compile-time checking and better OpenAPI metadata than returning IResult. Groups also let you apply authentication, rate limiting, and conventions once instead of per endpoint.
var api = app.MapGroup("/orders").RequireAuthorization();
api.MapGet("/{id:int}", (int id, IOrderService svc) =>
svc.Find(id) is { } o ? TypedResults.Ok(o) : Results.NotFound());
897. How do Blazor render modes change the way you design a page?
Render modes decide where a component executes: static SSR on the server, interactive server over a circuit, interactive WebAssembly in the browser, or auto which starts on the server and switches to WASM. Choosing per component means you must reason about state ownership, latency, and whether the component can run in the browser. Data access belongs behind an API when a component may become interactive WebAssembly.
898. When is static SSR the better Blazor choice than interactive server mode?
Static SSR renders HTML per request with no persistent circuit, so it scales cheaply and behaves like classic server-rendered pages. It suits content, forms with progressive enhancement, and public pages where interactivity is limited. Interactive server is better when you need rich client state and can afford long-lived connections.
899. What does HybridCache add over IMemoryCache plus IDistributedCache?
HybridCache combines a local in-memory layer with a distributed layer and handles stampede protection, serialization, and tag-based invalidation in one API. It removes the custom two-level cache code most teams write and get subtly wrong. The key caveat is that local entries can be stale across instances until invalidation propagates, so choose expiration values with that in mind.
var value = await cache.GetOrCreateAsync($"order:{id}",
async ct => await repo.LoadAsync(id, ct),
options: new() { Expiration = TimeSpan.FromMinutes(5) });
900. Which OpenTelemetry signals does .NET emit out of the box, and what still needs work?
The runtime and ASP.NET Core emit metrics for requests, GC, thread pool, and Kestrel, and HttpClient and EF Core provide traces and metrics with instrumentation packages. What still needs deliberate effort is business-level telemetry, correlation across async boundaries you own, and consistent resource attributes. Cardinality control is the usual production failure point.
901. How would you plan an upgrade from .NET Framework 4.8 to modern .NET?
Start with a dependency and API inventory: unsupported libraries, WCF, AppDomain, remoting, and Windows-only assumptions drive the estimate. Then choose a path, either a side-by-side rewrite of the web layer behind a proxy or an incremental migration using the compatibility shim for libraries. Running the test suite and load tests on the new runtime early exposes behavioural differences before the cutover.
902. What is your strategy for upgrading a large solution across several .NET versions?
Upgrade in dependency order, starting with leaf libraries and shared packages, and pin the SDK with a global.json so builds are reproducible. Use analyzers and the upgrade assistant to surface API changes, then run the full test suite per project rather than per solution. Keep the change mechanical and separate from feature work so reviews stay reviewable.
{ "sdk": { "version": "9.0.100", "rollForward": "latestFeature" } }
903. How do you decide whether a new language feature is safe to adopt in a shared codebase?
Adopt features that reduce ambiguity or boilerplate and that every target framework in the repo can compile, since multi-targeting constrains syntax. Check tooling support, including analyzers, formatters, and the debugging experience in production. Introduce one feature at a time and document the convention so the codebase does not become a mix of styles.
904. What does a good multi-stage Dockerfile for a .NET service look like?
Build and publish in an SDK stage, then copy only the published output into a runtime stage so the final image has no compilers or source. Use cache-friendly layer ordering by restoring against the project file before copying sources. Set a non-root user and explicit entrypoint in the final stage.
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled
WORKDIR /app
COPY --from=build /app ./
USER $APP_UID
ENTRYPOINT ["dotnet", "Api.dll"]
905. What are chiseled and distroless images, and why do they matter for security?
Chiseled images contain only the runtime, a minimal set of native libraries, and no shell or package manager, which shrinks the attack surface and image size. Distroless follows the same idea outside the Microsoft-published set. The trade-off is no shell for debugging, so you rely on exec into the app process, sidecar diagnostics, or an ephemeral debug container.
906. How do you reduce container image size and startup time for a .NET API?
Publish in Release with trimming or ReadyToRun where it helps, use the ASP.NET chiseled runtime image, and avoid copying test or source artifacts. Enabling composite or partial compilation of Razor views and precompiling serializers removes first-request work. Measure with a cold start on the target platform, since registry pull time often dominates the real deployment latency.
907. Which Kubernetes probes should a .NET service expose, and how do they differ?
Liveness answers whether the process should be restarted, readiness whether it should receive traffic, and startup whether the app has finished booting. Liveness must not check dependencies, or a database blip will restart every pod. Use the health checks package with separate tags and map them to distinct endpoints.
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddDbContextCheck<AppDb>(tags: ["ready"]);
app.MapHealthChecks("/health/live", new() { Predicate = r => r.Tags.Contains("live") });
app.MapHealthChecks("/health/ready", new() { Predicate = r => r.Tags.Contains("ready") });
908. How do you set resource requests and limits for a .NET pod without guessing?
Load test the service in a realistic environment and observe steady-state working set and CPU, then set requests near the observed baseline and limits with headroom. Memory limits below the real working set cause OOM kills, and .NET responds to container limits through the GC configuration. Revisit the numbers whenever the workload or runtime version changes.
909. When should configuration come from a ConfigMap and when from a Secret?
ConfigMaps hold non-sensitive settings such as feature toggles and endpoint URLs; Secrets hold credentials and keys, ideally sourced from an external secret store. Both should be mounted or projected rather than baked into images, so the same image runs in every environment. Prefer the configuration provider pattern with reload support so changes do not require a rebuild.
910. How does graceful shutdown work in ASP.NET Core on Kubernetes?
On SIGTERM, the host stops accepting new connections and waits for in-flight requests up to the shutdown timeout, while readiness should start failing so the endpoint is removed from service. The pod's termination grace period must exceed the application's shutdown timeout, otherwise requests are killed mid-flight. Long-running background work needs its own cancellation handling through IHostedService.StopAsync.
builder.Services.Configure<HostOptions>(o =>
o.ShutdownTimeout = TimeSpan.FromSeconds(25));
911. Why is a health endpoint that checks every dependency a production risk?
A readiness probe that fails when a non-critical dependency is slow will remove all instances at once, turning a partial degradation into an outage. Health checks should reflect the service's ability to serve traffic, not the health of the whole system. Separate critical dependencies from informational checks and alert on the latter instead of failing readiness.
912. How do you mitigate cold starts in Azure Functions?
Use the isolated worker model with the in-process alternatives, keep the deployment package small, avoid heavy static initialization, and prefer Premium or dedicated plans with pre-warmed instances for latency-sensitive paths. For HTTP APIs behind strict latency targets, a minimum instance count removes the problem at a cost. Measure the real cold start distribution rather than the best case.
913. How do you choose between Azure App Service, Container Apps, and AKS for a .NET workload?
App Service is the lowest-operations option for a single web app and handles scaling, slots, and certificates well. Container Apps adds container portability, revisions, and scale-to-zero with KEDA-style triggers without exposing Kubernetes. AKS is justified when you need the Kubernetes ecosystem, custom operators, or fine-grained networking and scheduling control, and you accept the platform ownership cost.
914. How does managed identity change the way you handle secrets in Azure?
A system-assigned or user-assigned identity lets the workload obtain tokens from Microsoft Entra ID without storing credentials, and DefaultAzureCredential picks it up locally and in the cloud. The remaining configuration is only the resource endpoint and the role assignment. This removes connection-string secrets from configuration and rotation from your runbooks.
var client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential());
var secret = await client.GetSecretAsync("db-password");
915. What is the difference between build-once-deploy-many and rebuilding per environment?
Build once produces a single immutable artifact promoted through environments, so what you tested is exactly what runs in production. Rebuilding per environment reintroduces risk from dependency drift and makes provenance hard to prove. Environment differences belong in configuration and secrets, not in the build.
916. How do you version container images and build artifacts reliably?
Tag images with the commit SHA as the immutable identifier and add a human-readable semver or build number as a moving convenience tag. Never deploy the latest tag to production, since it cannot be traced back to a source revision. Also record the artifact digest in the deployment record for auditability.
917. What does a solid GitHub Actions pipeline for a .NET service include?
Separate build, test, and deploy jobs with dependency caching, a build once into a container or artifact, and environment-scoped approvals for production. Use OIDC federation to the cloud instead of stored credentials, and fail the build on vulnerable packages and analyzers. Keep the workflow itself reviewed and versioned like code.
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '9.0.x' }
- run: dotnet test --configuration Release --logger trx
918. How do Azure DevOps pipelines differ from GitHub Actions in ways that matter for .NET teams?
Azure DevOps offers environments with approvals, checks, and gates that suit regulated release processes, plus tighter integration with Boards and test plans. GitHub Actions has a larger marketplace and simpler YAML reuse through composite actions. The decision is usually organizational rather than technical, so standardize on one to avoid dual maintenance.
919. How do feature flags support progressive delivery without redeploying?
Flags separate deployment from release, so code can ship dark and be enabled for a cohort later. Use a flag service with targeting rules and an audit trail, and keep flag evaluation cheap and cached. Every temporary flag needs an owner and an expiry, otherwise the codebase fills with dead branches.
920. How does a canary deployment differ from blue-green?
Blue-green runs two complete environments and switches traffic in one step, giving instant rollback but doubling infrastructure. Canary sends a small percentage of traffic to the new version and increases it while watching error rates and latency. Canary is better for uncertain behaviour changes, blue-green for fast atomic cutovers with cheap rollback.
921. How should database migrations be executed in a CI/CD pipeline?
Migrations should run as an explicit, idempotent step before the new application version takes traffic, with a lock so concurrent instances do not race. They must be backward compatible: add nullable columns and new tables first, backfill, then remove old structures in a later release. Never let application startup apply migrations implicitly in a multi-instance deployment.
dotnet ef migrations bundle -o migrate -r linux-x64
./migrate --connection "$CONN"
922. What makes a database change safe for zero-downtime deployment?
The expand-migrate-contract pattern keeps old and new code working simultaneously: expand the schema, migrate data in batches, then contract once no instance uses the old shape. Avoid long locks on large tables, and add indexes concurrently where the engine supports it. Every change should be reversible or have a documented forward-fix plan.
923. What belongs in infrastructure as code, and what does not?
Networking, compute, identity, and data services should be declared in code with remote state, review, and drift detection. Application configuration that changes frequently and secrets belong in a configuration store referenced by the IaC, not hard-coded. Environment-specific values should be parameterized rather than forked into separate templates.
924. What does environment parity actually require, and where do teams over-invest?
Parity means the same runtime version, container image, and deployment mechanism across environments, with differences limited to scale, data, and secrets. It does not require identical data volumes or identical instance counts, which are expensive and rarely the source of failures. Most parity bugs come from differing configuration and dependency versions, not from size.
925. What makes a production dashboard useful rather than decorative?
A good dashboard answers a small set of questions: are users succeeding, is latency within SLO, what changed recently. Put SLO burn rate and error budget at the top, then dependency health, then saturation signals. Every panel should link to a runbook or query so an on-call engineer can act without guessing.
926. What should a runbook contain for a .NET service?
It should list symptoms, the queries and dashboards to confirm them, the immediate mitigation steps such as scaling out or rolling back, and the escalation path. Include known failure modes with their signatures and the commands or links to execute them. Runbooks decay, so assign ownership and review them after every incident.
927. How do you approach cost awareness in a cloud-hosted .NET estate?
Right-size based on measured utilization, use autoscaling with sensible minimums, and shut down non-production outside working hours. Tag resources by owner and environment so cost can be attributed, and review the largest line items monthly rather than chasing small ones. Watch for idle premium tiers, over-provisioned databases, and log retention that nobody reads.
928. Why is autoscaling often ineffective without the right metrics?
Scaling on CPU alone misses I/O-bound or queue-driven workloads, so the service stays slow while CPU looks idle. Choose signals that correlate with load, such as request concurrency, queue depth, or custom business metrics. Also verify that scale-out latency is shorter than the traffic spike, otherwise the autoscaler always reacts too late.
929. How does a layered architecture differ from clean or hexagonal architecture?
Layering organizes code by technical concern, and dependencies typically flow downward with the data layer often leaking into business logic. Clean and hexagonal put the domain at the center and make infrastructure depend on abstractions owned by the domain. The practical difference is dependency direction and testability, not the number of folders.
930. What problem does the vertical slice architecture solve?
Vertical slices organize code by feature so a request's endpoint, handler, validation, and persistence live together, which reduces cross-cutting churn. It trades reuse for locality and makes features easy to delete or extract later. Shared infrastructure still belongs in a common layer, but domain concepts stay inside the slice.
931. When is a modular monolith a better choice than microservices?
A modular monolith gives strong module boundaries inside one deployable, avoiding distributed transactions, network failures, and operational overhead while the domain is still being understood. It suits teams without platform capacity or with unclear service boundaries. Because modules communicate in process, you can extract a service later once the boundary is proven.
932. What are the real costs of microservices that teams underestimate?
Distributed debugging, eventual consistency, contract versioning, and per-service operational tooling usually cost more than the code. Each service multiplies deployment pipelines, dashboards, on-call surface, and data ownership decisions. Microservices pay off only when independent scaling and autonomous team delivery genuinely matter.
933. What are the trade-offs of CQRS, and when does it hurt?
CQRS separates write models from read models, allowing each to be optimized and scaled independently. It hurts when the domain is simple, because you duplicate models and introduce synchronization and eventual consistency for no benefit. Apply it to specific bounded contexts with asymmetric read and write loads, not as a global default.
934. When is event sourcing justified over state-stored persistence?
Event sourcing is justified when you need a complete audit trail, temporal queries, or the ability to rebuild projections for new read models. The costs are event schema evolution, snapshotting, replay time, and a steeper learning curve for the team. Most systems need an audit log, not an event-sourced domain model.
935. Which DDD tactical patterns do you actually use in a .NET codebase?
Aggregates with invariants enforced in the root, value objects for concepts without identity, and domain events for cross-aggregate reactions cover most needs. Repositories are useful at aggregate boundaries but often unnecessary over EF Core's DbContext. Factories and specifications are worth adding only when the complexity they remove is real.
936. What is an anti-corruption layer and where does it sit?
It is a translation boundary that protects your domain model from a foreign model, such as a legacy system or third-party API. It maps external contracts and semantics into your own types and isolates change. It usually sits in infrastructure, exposing a domain-owned interface to the application layer.
937. How does the strangler fig pattern work for migrating a monolith?
You put a facade or router in front of the monolith and route selected capabilities to new services, growing the new system until the old one can be retired. Each step is small, reversible, and shippable, which keeps risk low. The facade is temporary and must be designed to disappear, not to become a permanent integration hub.
938. Why use the transactional outbox pattern with EF Core?
The outbox writes domain changes and outgoing messages in the same database transaction, then a background publisher forwards them, so you never lose a message after a commit. It converts dual-write inconsistency into at-least-once delivery, which consumers must handle idempotently. A relay or CDC process and a cleanup job are required pieces, not optional extras.
await using var tx = await db.Database.BeginTransactionAsync(ct);
db.Orders.Add(order);
db.Outbox.Add(new OutboxMessage(order.Id, OrderCreated));
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
939. What are the failure modes of a transactional outbox implementation?
Duplicate delivery is guaranteed at-least-once, so consumers need deduplication keys. Ordering can break if the relay publishes in parallel, and a backlog grows silently if the publisher stops. Monitor outbox lag and dead-letter poison messages, and cap retries with an alert.
940. When does an API gateway or BFF make sense?
A BFF gives each client type a tailored aggregation layer, avoiding chatty calls and over-fetching from mobile or web clients. An API gateway centralizes cross-cutting concerns such as authentication, rate limiting, and routing. Both risk becoming a shared bottleneck where every team's changes collide, so keep ownership clear.
941. How do you handle partial failures in API composition?
Decide per call whether the response is degraded-but-usable or must fail, and apply timeouts, circuit breakers, and fallbacks accordingly. Return partial results with explicit metadata rather than silently omitting data. Cache the expensive composed pieces so a single slow dependency does not take down the whole response.
942. What does caching at the edge change about your architecture?
Edge caching removes load and latency for cacheable content, but it makes invalidation the hard problem and complicates per-user responses. You need explicit cache keys, correct Vary headers, and short TTLs for anything personalized. It also means origin outages become partially invisible, so observability must distinguish edge hits from origin errors.
943. What is different about designing for multi-region active-active?
Active-active requires conflict-free data models or a single writer per partition, because latency makes synchronous cross-region consistency impractical. Routing needs to consider data residency and failover semantics, not just proximity. Many teams are better served by active-passive with a tested failover than by true active-active.
944. How do data residency requirements constrain your cloud design?
Data must stay in permitted geographies, which affects region selection, replication, backup storage, and even support access. It can force per-region data stores and careful handling of caches, logs, and telemetry that may carry personal data. Document the data flow per component, since incidental copies are the usual compliance gap.
945. How do RPO and RTO drive your disaster recovery design?
RPO defines acceptable data loss and drives backup frequency and replication mode; RTO defines acceptable downtime and drives standby capacity and automation. A warm standby with automated failover costs more but meets tight targets, while backup-and-restore is cheap and slow. Whatever the design, the only proof is a rehearsed failover with measured timings.
946. How do you keep technical debt visible and managed rather than rhetorical?
Record debt items with impact, effort, and the risk of deferral, then allocate a fixed capacity share each iteration. Tie items to incidents or delivery friction so prioritization is evidence-based. Track trends such as change failure rate and lead time to show whether the debt work is paying off.
947. What does an effective code review focus on in a .NET team?
Review for correctness, boundary and contract design, error handling, security, and test coverage of the change's intent. Style and formatting belong to automated tooling so human attention goes to design. Keep changes small and review within a day, since large late reviews produce shallow feedback.
948. What makes an architecture decision record useful years later?
An ADR captures the context, the decision, the alternatives considered, and the consequences, including what you are now constrained from doing. It should be short, dated, immutable, and superseded rather than edited when things change. The value comes from recording why, since the what is visible in the code.
949. How do you incrementally extract a service from a monolith without a big-bang rewrite?
Start with a capability that has clear boundaries and low coupling, put a router in front, and move reads before writes to reduce risk. Keep the data ownership transition explicit, often with dual writes or a change data capture bridge during the transition. Ship each slice to production and measure before starting the next.
950. Which .NET anti-patterns do you most often find in code review?
Captive dependencies from resolving scoped services in singletons, async over sync with blocking calls, and service locator usage are the classics. Others include anemic domain models with logic in controllers, catch-all exception handling, and static mutable state. Most are detectable with analyzers, DI validation on build, and a strict code review checklist.
951. How do you detect a captive dependency in ASP.NET Core before production?
Enable scope validation in the development environment and build the service provider at startup with ValidateOnBuild and ValidateScopes. Then resolve each root service once so the container walks the graph. Integration tests that boot the real host catch most of these earlier than production traffic.
builder.Host.UseDefaultServiceProvider(o =>
{
o.ValidateScopes = true;
o.ValidateOnBuild = true;
});
952. Why is async void dangerous outside event handlers?
An async void method cannot be awaited, so exceptions escape to the synchronization context and can crash the process. Callers also cannot observe completion or apply backpressure. Use async Task everywhere except framework event handlers, and wrap those bodies in try/catch.
953. What is the practical difference between IHttpClientFactory and a singleton HttpClient?
A singleton HttpClient does not respect DNS changes and can accumulate stale connections, while creating one per call exhausts sockets. IHttpClientFactory pools handlers, rotates them, and integrates with Polly-style resilience pipelines. Use a named or typed client and configure timeouts and resilience at registration.
954. How do you add resilience to outgoing HTTP calls in modern .NET?
Use the Microsoft.Extensions.Http.Resilience package to add retries with jitter, circuit breaking, timeouts, and hedging through a standard pipeline. Retries must be limited to idempotent operations and combined with a total timeout so they cannot amplify an outage. Emit telemetry for each strategy so you can see when the breaker opens.
builder.Services.AddHttpClient<CatalogClient>()
.AddStandardResilienceHandler();
955. How do you prevent a retry storm during a dependency outage?
Combine a circuit breaker with bounded retries, exponential backoff, and jitter, and cap the total time budget per request. Queue-based work should back off at the consumer level rather than retrying in a tight loop. Alert on breaker state transitions so the team sees the outage before users do.
956. What does idempotency require at the API level for POST operations?
The client supplies an idempotency key, and the server stores the key with the resulting response so retries return the original outcome instead of creating duplicates. The key store needs a TTL and must be written in the same transaction as the business change. Without this, at-least-once messaging and client retries will eventually duplicate work.
957. How do you choose between synchronous and asynchronous service-to-service communication?
Synchronous calls are simpler to reason about and give immediate feedback, but they couple availability and latency across services. Asynchronous messaging decouples them at the cost of eventual consistency, ordering, and idempotency work. Use synchronous for queries the user is waiting on and asynchronous for state changes that can complete later.
958. What is the difference between choreography and orchestration in a distributed workflow?
Choreography lets services react to events with no central coordinator, which is loosely coupled but hard to trace end to end. Orchestration uses a saga or workflow engine that explicitly drives steps and compensations. Complex multi-step business processes with strict timeouts usually benefit from orchestration, while simple fan-out reactions fit choreography.
959. How do you implement a saga and its compensations in .NET?
Model each step with a forward action and a compensating action, persist saga state, and make every step idempotent so retries are safe. Use a durable store or workflow engine so a process restart does not lose progress. Compensation is business logic, not a rollback, so it must be designed with domain experts.
960. What belongs in a domain event versus an integration event?
Domain events are in-process facts raised inside a bounded context, while integration events cross process boundaries and form a public contract. Integration events need versioning, stable schemas, and a publish mechanism such as an outbox. Do not publish domain types directly, since internal refactoring would break consumers.
961. How do you version a public HTTP API without breaking clients?
Prefer additive change: new optional fields, new endpoints, and tolerant readers that ignore unknown members. When a breaking change is unavoidable, use explicit versioning in the path or header and run both versions concurrently with a deprecation timeline. Track client usage per version so you know when it is safe to retire one.
962. How do you design a message schema that can evolve safely?
Treat the schema as a contract: add optional fields, never rename or repurpose existing ones, and keep consumers tolerant of unknown properties. Use a schema registry with compatibility checks in CI when the broker supports it. Include a version and correlation identifiers in the envelope rather than the payload.
963. What are poison messages and how do you handle them?
A poison message fails repeatedly and blocks progress if retried forever. Use a bounded retry count with dead-lettering, then inspect and replay from the dead-letter queue after a fix. Alert on dead-letter depth, since silent accumulation is usually the first sign of a contract mismatch.
964. How do you achieve exactly-once effects on top of at-least-once delivery?
You cannot get exactly-once delivery end to end, so make handlers idempotent using a deduplication store keyed by message id or a natural business key. Combine that with an inbox table written in the same transaction as the business change. The result is effectively-once processing, which is what the requirement actually means.
965. How do you handle distributed transactions now that MSDTC is rarely an option?
Replace them with sagas, outbox patterns, and idempotent compensating actions, accepting eventual consistency. Where strong consistency is essential, keep the data inside one service and one database boundary. Designing boundaries so transactions stay local is cheaper than making distributed transactions reliable.
966. What is your approach to connection pooling and DbContext lifetime in a web API?
Register DbContext as scoped so each request gets a clean unit of work, and let the provider's connection pool handle physical connections. Avoid keeping a DbContext alive across background work or parallel tasks, since it is not thread-safe. For high-throughput read paths, consider compiled queries or Dapper rather than fighting the change tracker.
967. How do you diagnose high memory usage in a .NET service in production?
Start with runtime metrics and GC counters to distinguish a leak from expected generation growth, then capture a dump or use dotnet-counters and dotnet-gcdump on a suspect instance. Analyze retention paths to find what is holding objects alive, often event handler subscriptions, caches, or static collections. Fix the root cause rather than raising the container limit.
968. Which signals tell you a service needs to be refactored rather than tuned?
Rising change failure rate, tests that break for unrelated reasons, and features that require touching many modules point to structural problems. Performance tuning rarely fixes growing coordination cost. Use lead time and defect clustering per module as evidence when proposing refactoring work.
969. How do you structure configuration for multiple environments in ASP.NET Core?
Layer configuration sources so the environment-specific provider overrides appsettings defaults, and bind strongly typed options with validation at startup. Fail fast on invalid configuration rather than discovering it at first request. Keep secrets out of source and inject them through the environment or a secret store.
builder.Services.AddOptions<PaymentsOptions>()
.Bind(builder.Configuration.GetSection("Payments"))
.ValidateDataAnnotations()
.ValidateOnStart();
970. Why is validating options at startup better than lazy validation?
ValidateOnStart turns a misconfiguration into a failed deployment instead of a runtime error under specific traffic. It also makes the failure visible in the orchestrator's startup logs and health checks. Lazy validation is only reasonable for optional features that are not always exercised.
971. How do you keep a background service from blocking application shutdown?
Implement BackgroundService with a proper cancellation token loop and respect the stopping token in all awaited calls. Long-running work should checkpoint so it can resume rather than restart from scratch. Never swallow the cancellation exception or the host will hang until the grace period expires.
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await _worker.RunOnceAsync(ct);
await Task.Delay(_interval, ct);
}
}
972. When should work move out of the request pipeline into a queue?
Move work out when it is slow, retryable, or not needed for the response, such as emails, document generation, or third-party sync. This shortens response times and isolates failures. The cost is a queue to operate, plus the need for idempotency and status reporting to the user.
973. How do you implement rate limiting fairly across tenants?
Use the built-in rate limiter with partition keys derived from tenant identity rather than IP, and choose a policy such as sliding window or token bucket per tier. Return 429 with Retry-After so clients can back off correctly. Distributed enforcement needs a shared store, since per-instance limits multiply with scale.
builder.Services.AddRateLimiter(o => o.AddPolicy("tenant", ctx =>
RateLimitPartition.GetTokenBucketLimiter(ctx.User.FindFirst("tid")!.Value, _ => new()
{
TokenLimit = 100, ReplenishmentPeriod = TimeSpan.FromSeconds(1), TokensPerPeriod = 10
})));
974. What is the correct way to store passwords and other secrets in a .NET application?
Passwords are hashed with a slow, salted algorithm such as Argon2id or PBKDF2 with a current work factor, never encrypted reversibly. Service credentials belong in a managed secret store accessed through managed identity. Application code should never contain keys, and configuration files should not be the source of truth for production secrets.
975. How do you approach authentication and authorization for internal service calls?
Use OAuth 2 client credentials or workload identity so each service has its own identity and token audience. Authorize on scopes or application roles rather than network location, since network trust fails silently. Token caching and clock skew handling are small details that cause large outages if ignored.
976. What is the difference between authentication middleware order mistakes and authorization bugs?
Middleware order determines whether identity is established before authorization or endpoint execution, and getting it wrong yields 401s or anonymous access. UseRouting must precede UseAuthorization, and authentication must precede both. Add a startup test that asserts a protected endpoint returns 401 without a token.
977. How do you test a service that depends on time, randomness, and external HTTP?
Inject TimeProvider for time, an abstraction for randomness or ID generation, and a stubbed HttpMessageHandler or WireMock for HTTP. This keeps tests deterministic and fast without network access. Reserve full integration tests with real infrastructure for a small number of critical paths.
978. What is the difference between a unit test and a component test in a .NET solution?
A unit test exercises a small piece in isolation with collaborators replaced, while a component test boots the real host with its real DI graph and often a real database. Component tests catch configuration and wiring errors that unit tests never see. Both are needed, but the pyramid should still be bottom-heavy.
979. How do you test EF Core queries meaningfully?
Test against the real provider, since the in-memory provider does not enforce relational semantics such as constraints, translations, or cascades. Use a disposable database, ideally in a container, and reset state between tests. Assert on the resulting data rather than on generated SQL, except where a performance regression matters.
980. How do you keep integration tests fast in a busy CI pipeline?
Share a containerized database across the test run with per-test schema isolation or transactions, and run independent collections in parallel. Cache images and package restores between runs. Split slow end-to-end tests into a separate stage so fast feedback stays under a few minutes.
981. How do you introduce contract testing between a producer and its consumers?
Consumers publish expectations as contracts, and the producer's build verifies them so breaking changes fail before deployment. This replaces a large end-to-end environment with fast, targeted checks. Start with the highest-traffic consumer paths and keep contracts versioned alongside the code.
982. What is your approach to load testing before a major release?
983. How do you decide the right caching layer for a given read path?
Start with the consistency requirement and the tolerance for staleness, then choose memory, distributed cache, HTTP caching, or a CDN accordingly. Every cache needs an invalidation story and a bound on size. Measure the hit rate before adding complexity, since the fastest cache is often a better query.
984. What are the risks of caching per-request data in a singleton service?
It leaks data across users and grows without bound, and it often hides a scoping bug that will surface as a security incident. HttpContext and scoped services must never be captured in singletons. Use IMemoryCache with explicit keys that include the tenant or user, plus size limits and expiration.
985. How do you design pagination for a large, frequently changing dataset?
Prefer keyset pagination on an indexed, stable sort key, since offset pagination degrades and duplicates rows when data shifts. Return an opaque cursor so the ordering can evolve without breaking clients. Cap page sizes and document the ordering guarantee.
var page = await db.Orders
.Where(o => o.Id > cursor)
.OrderBy(o => o.Id)
.Take(50)
.ToListAsync();
986. How do you handle large file uploads and downloads in an API?
Stream directly to blob storage using pre-signed URLs or streaming endpoints rather than buffering in memory, and enforce size limits and content validation. Serve downloads from the storage or CDN edge to keep the app tier out of the data path. Add virus scanning and authorization checks as asynchronous steps.
987. What does a well-designed error response look like?
Use consistent problem details with a machine-readable code, a human-readable message, and a correlation id for support. Never leak stack traces or internal identifiers. Distinguish client errors from server errors so clients can decide whether to retry.
return TypedResults.Problem(
statusCode: 409, title: "Duplicate order",
extensions: new Dictionary<string, object?> { ["code"] = "ORDER_EXISTS" });
988. How do you correlate logs across services without leaking sensitive data?
Propagate a trace context through W3C headers and include trace and span ids in structured logs. Log identifiers rather than payloads, and scrub personal data at the sink. Set sampling so volume stays manageable while errors are always retained.
989. Why is structured logging preferable to string interpolation in production?
Structured logging keeps properties queryable, so you can filter by tenant, order id, or duration instead of regex-matching text. It also lets sinks redact fields consistently. Message templates avoid the allocation cost of building strings that may never be written.
logger.LogInformation("Order {OrderId} shipped in {ElapsedMs} ms", orderId, elapsed);
990. How do you keep log volume and cost under control?
Set levels per namespace, sample high-frequency informational logs, and drop health check noise at the sink. Retain debug logs briefly and errors longer, based on actual investigation needs. Review the top log producers monthly, since one verbose component often dominates the bill.
991. What is the role of SLOs and error budgets in delivery decisions?
An SLO sets a measurable reliability target, and the error budget quantifies allowed failure within a period. When the budget is exhausted, feature work pauses in favour of reliability work, which removes the argument. SLOs should be based on user-visible signals rather than internal CPU metrics.
992. How do you run a useful post-incident review?
Focus on contributing factors and detection gaps rather than individual blame, and produce a small number of owned, dated actions. Include the timeline, the customer impact, and what made recovery slow. Track the actions to completion, otherwise the same incident recurs.
993. What is the difference between horizontal and vertical scaling limits for a .NET API?
Vertical scaling adds resources to one instance and hits a hard ceiling with rising cost, while horizontal scaling adds instances and requires statelessness. Anything held in process memory, such as sessions or local caches, breaks horizontal scaling. Data-tier scaling is usually the real bottleneck.
994. How do you migrate session state out of a single server?
Move to a distributed cache or, better, a stateless design using tokens or signed cookies so no server holds session data. Sticky sessions are a stopgap that complicates deployments and failover. Encrypt and keep session payloads small, since every request pays for the round trip.
995. How do you decide whether to use a monorepo or multiple repositories?
A monorepo simplifies atomic cross-service changes and shared tooling, but needs build graph optimization to stay fast. Multiple repos enforce boundaries and independent release cadence at the cost of coordinated versioning. The deciding factor is usually how often changes span boundaries, not repository size.
996. How do you manage shared library versioning across many services?
Keep shared libraries small and focused on cross-cutting concerns, version them semantically, and publish packages from CI. Consumers should upgrade deliberately rather than float on a wildcard version. Too much shared code turns into a distributed monolith with lockstep releases.
997. What is the danger of a shared internal framework package?
It centralizes decisions that teams should own, and a breaking change forces every service to upgrade together. It also tends to accumulate unrelated helpers until no one understands it. Prefer small composable packages or templates over a single internal framework.
Building blocks 14 questions
Copy-usable .NET recipes: controller and service skeleton, minimal API, DbContext configuration, DI registration, middleware, exception handling, JWT, policies, OpenAPI, keyset pagination, health checks, HttpClient, background service, integration test.
998. How do I keep a controller thin by separating services and DTOs from the API layer?
Keep the controller responsible only for HTTP concerns: read the request, call an application service, and return a result. Map entities to DTOs inside the service so persistence models never leak to the client.
public record ProductDto(int Id, string Name, decimal Price);
public interface IProductService
{
Task<ProductDto?> GetAsync(int id, CancellationToken ct);
}
public sealed class ProductService(AppDbContext db) : IProductService
{
public async Task<ProductDto?> GetAsync(int id, CancellationToken ct) =>
await db.Products.AsNoTracking()
.Where(p => p.Id == id)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.FirstOrDefaultAsync(ct);
}
[ApiController]
[Route("api/products")]
public sealed class ProductsController(IProductService service) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<ActionResult<ProductDto>> Get(int id, CancellationToken ct)
{
var dto = await service.GetAsync(id, ct);
return dto is null ? NotFound() : Ok(dto);
}
}
999. How do I build a minimal API endpoint with validation and TypedResults?
Validate the model explicitly and return TypedResults so the handler has a strongly typed result and accurate OpenAPI metadata. Map validation failures to ValidationProblem instead of throwing.
public record CreateOrderRequest(string Customer, int Quantity);
public static class OrderEndpoints
{
public static RouteGroupBuilder MapOrders(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/orders");
group.MapPost("/", async (CreateOrderRequest request, IOrderService service, CancellationToken ct) =>
{
if (string.IsNullOrWhiteSpace(request.Customer) || request.Quantity <= 0)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["customer"] = ["Customer is required."],
["quantity"] = ["Quantity must be greater than zero."]
});
}
var id = await service.CreateAsync(request, ct);
return TypedResults.Created($"/orders/{id}", id);
});
return group;
}
}
1000. How do I configure EF Core entities with IEntityTypeConfiguration instead of a bloated DbContext?
Move fluent configuration into one class per entity and let ApplyConfigurationsFromAssembly discover them. This keeps OnModelCreating to a single line and makes each mapping independently testable.
public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.Customer).HasMaxLength(200).IsRequired();
builder.Property(o => o.Total).HasPrecision(18, 2);
builder.HasIndex(o => new { o.Customer, o.CreatedAt });
builder.HasMany(o => o.Lines)
.WithOne()
.HasForeignKey(l => l.OrderId)
.OnDelete(DeleteBehavior.Cascade);
}
}
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
1001. How do I split dependency injection registration into reusable extension methods?
Group registrations by feature in static extension methods on IServiceCollection and compose them from Program. This keeps startup code readable and lets tests register the same slices.
public static class OrderModule
{
public static IServiceCollection AddOrderModule(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderRepository, EfOrderRepository>();
services.AddOptions<OrderOptions>()
.Bind(config.GetSection("Orders"))
.ValidateDataAnnotations()
.ValidateOnStart();
return services;
}
}
public static class InfrastructureModule
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<AppDbContext>(o => o.UseSqlServer(config.GetConnectionString("Default")));
return services;
}
}
// Program.cs
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddOrderModule(builder.Configuration);
1002. How do I add a correlation id to every request with custom middleware?
Read an incoming correlation header or generate a new id, store it in HttpContext.Items, and echo it back on the response. Register the middleware early so downstream logging picks the value up.
public sealed class CorrelationIdMiddleware(RequestDelegate next)
{
public const string HeaderName = "X-Correlation-Id";
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers.TryGetValue(HeaderName, out var incoming)
&& !string.IsNullOrWhiteSpace(incoming)
? incoming.ToString()
: Guid.NewGuid().ToString("N");
context.Items[HeaderName] = correlationId;
context.Response.Headers[HeaderName] = correlationId;
using (context.RequestServices.GetRequiredService<ILogger<CorrelationIdMiddleware>>()
.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
await next(context);
}
}
}
// Program.cs
app.UseMiddleware<CorrelationIdMiddleware>();
1003. How do I return consistent ProblemDetails for unhandled exceptions using IExceptionHandler?
Implement IExceptionHandler, map known exception types to status codes, and register it with AddExceptionHandler plus UseExceptionHandler. The framework then writes a ProblemDetails payload for you.
public sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken ct)
{
logger.LogError(exception, "Unhandled exception for {Path}", context.Request.Path);
var (status, title) = exception switch
{
KeyNotFoundException => (StatusCodes.Status404NotFound, "Resource not found"),
ArgumentException => (StatusCodes.Status400BadRequest, "Invalid request"),
_ => (StatusCodes.Status500InternalServerError, "Unexpected error")
};
var problem = new ProblemDetails
{
Status = status,
Title = title,
Detail = context.TraceIdentifier
};
problem.Extensions["correlationId"] = context.Items["X-Correlation-Id"];
context.Response.StatusCode = status;
await context.Response.WriteAsJsonAsync(problem, ct);
return true;
}
}
// Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
1004. How do I set up JWT bearer authentication for protected endpoints?
Register the JWT bearer scheme with token validation parameters that mirror the issuer, then add authentication and authorization to the pipeline. Keep the signing key in configuration or a secret store, never in source.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
ClockSkew = TimeSpan.FromSeconds(30)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/me", (ClaimsPrincipal user) => user.Identity?.Name)
.RequireAuthorization();
1005. How do I implement policy-based authorization with a custom requirement and handler?
Define a requirement class, write a handler that evaluates it against the user and resource, then register the policy with its requirement. Policies keep authorization rules out of controllers and make them unit-testable.
public sealed record MinimumAgeRequirement(int Age) : IAuthorizationRequirement;
public sealed class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
var claim = context.User.FindFirst("birthdate")?.Value;
if (DateTime.TryParse(claim, out var birthdate) &&
DateTime.Today.AddYears(-requirement.Age) >= birthdate)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddAuthorization(options =>
options.AddPolicy("Adult", policy =>
policy.RequireAuthenticatedUser().AddRequirements(new MinimumAgeRequirement(18))));
app.MapGet("/adults-only", () => "ok").RequireAuthorization("Adult");
1006. How do I add OpenAPI metadata to both controllers and minimal API endpoints?
Register OpenAPI generation, add annotations to endpoints, and use response attributes on controllers. Descriptive metadata improves generated clients and the interactive document.
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapPost("/orders", async (CreateOrderRequest request, IOrderService service, CancellationToken ct) =>
{
var id = await service.CreateAsync(request, ct);
return TypedResults.Created($"/orders/{id}", id);
})
.WithName("CreateOrder")
.WithTags("Orders")
.WithSummary("Creates a new order")
.Produces<int>(StatusCodes.Status201Created)
.ProducesValidationProblem();
[ApiController]
[Route("api/products")]
[Produces("application/json")]
public sealed class ProductsController(IProductService service) : ControllerBase
{
[HttpGet("{id:int}")]
[ProducesResponseType<ProductDto>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProductDto>> Get(int id, CancellationToken ct)
{
var dto = await service.GetAsync(id, ct);
return dto is null ? NotFound() : Ok(dto);
}
}
1007. How do I implement keyset pagination instead of offset paging?
Order by a unique sort key, filter with a WHERE key > lastKey predicate, and take pageSize + 1 rows to know whether another page exists. Keyset paging stays fast on large tables because it avoids deep offsets.
public sealed record Page<T>(IReadOnlyList<T> Items, int? NextKey);
public static class KeysetPaging
{
public static async Task<Page<Order>> GetPageAsync(
AppDbContext db, int? lastKey, int pageSize, CancellationToken ct)
{
var query = db.Orders.AsNoTracking().OrderBy(o => o.Id);
if (lastKey is not null)
{
query = (IOrderedQueryable<Order>)query.Where(o => o.Id > lastKey);
}
var rows = await query.Take(pageSize + 1).ToListAsync(ct);
var hasMore = rows.Count > pageSize;
if (hasMore)
{
rows.RemoveAt(rows.Count - 1);
}
int? nextKey = hasMore ? rows[^1].Id : null;
return new Page<Order>(rows, nextKey);
}
}
1008. How do I expose separate readiness and liveness health checks?
Tag checks by purpose and map two endpoints with different tags, so an orchestrator can restart on liveness failures but only remove traffic on readiness failures. Keep liveness checks dependency-free.
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddDbContextCheck<AppDbContext>("database", tags: ["ready"])
.AddUrlGroup(new Uri(builder.Configuration["Dependencies:Payments"]!), "payments", tags: ["ready"]);
var app = builder.Build();
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new { status = report.Status.ToString() });
}
});
1009. How do I call an external API through IHttpClientFactory with retries and a timeout?
Register a named or typed client, attach a resilience handler for retries and a timeout, then inject the typed client. Never create HttpClient instances manually inside request handlers.
public sealed class PaymentsClient(HttpClient http)
{
public async Task<PaymentResult?> ChargeAsync(ChargeRequest request, CancellationToken ct)
{
var response = await http.PostAsJsonAsync("v1/charges", request, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentResult>(ct);
}
}
// Program.cs
builder.Services.AddHttpClient<PaymentsClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Payments:BaseUrl"]!);
client.Timeout = TimeSpan.FromSeconds(10);
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
});
1010. How do I write a BackgroundService that shuts down gracefully on cancellation?
Pass the stoppingToken into every awaited call so shutdown interrupts the loop promptly, and log exceptions without letting them kill the host. Wrap the loop body in a try/catch that respects cancellation.
public sealed class OutboxPublisher(IServiceScopeFactory scopeFactory, ILogger<OutboxPublisher> logger)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = scopeFactory.CreateScope();
var publisher = scope.ServiceProvider.GetRequiredService<IOutboxPublisher>();
await publisher.PublishPendingAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Outbox publish cycle failed");
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
// Program.cs
builder.Services.AddHostedService<OutboxPublisher>();
1011. How do I write an integration test that calls a real endpoint with WebApplicationFactory?
Use WebApplicationFactory<Program> to boot the real pipeline in memory, then issue an HTTP request through the created client. Override services when you need to swap a real dependency such as the database.
public sealed class OrdersApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Create_order_returns_created()
{
using var client = factory.CreateClient();
var request = new CreateOrderRequest("ACME", 2);
var response = await client.PostAsJsonAsync("/orders", request);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var id = await response.Content.ReadFromJsonAsync<int>();
Assert.True(id > 0);
}
}
// Swap infrastructure in a derived factory when needed
public sealed class TestAppFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
services.RemoveAll<DbContextOptions<AppDbContext>>());
}
}