Interview Questions & Answers
Interview Questions & Answers
NET Senior
Interview Questions & Answers
215 Real-World Questions with Clear Answers
2025 Edition
Q1 What is the difference between a value type and a reference type in C#?
Answer
Value types live on the stack (or inline inside an object). Copying one gives you a completely independent copy —
changing the copy leaves the original untouched.
Reference types live on the heap. Assigning one to another variable just copies the pointer, so both variables point
at the same object. Mutating through one is visible through the other.
Common value types: int, bool, double, struct, enum. Common reference types: class, string, array, delegate.
Example
int a = 5;
int b = a; // independent copy
b = 10;
[Link](a); // still 5
Answer
float (32-bit) and double (64-bit) are binary floating-point — fast but imprecise for exact decimal fractions. decimal
(128-bit) uses base-10, so 0.1 + 0.2 is exactly 0.3.
Rule of thumb: scientific/graphics work use double. Money and financial figures use decimal. float is rarely needed
except for high-volume GPU work.
Example
double d = 0.1 + 0.2;
[Link](d); // 0.30000000000000004
Answer
string is immutable — every concatenation creates a new heap object. For a few literals that is fine; inside a loop
with thousands of appends it causes a heap explosion.
StringBuilder keeps one mutable internal buffer and only allocates when it fills up. Call ToString() once at the end.
Example
// Bad in a tight loop
string s = "";
for (int i = 0; i < 10_000; i++) s += i; // 10,000 allocs
// Good
var sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) [Link](i);
string result = [Link](); // 1 alloc
Answer
Boxing wraps a value type in a heap object so it can be passed as object. Unboxing casts it back. Each operation
involves a heap allocation plus a copy — expensive on hot paths.
Classic trap: ArrayList boxes every int you put in. Use List<int> instead.
Example
int x = 42;
object boxed = x; // boxing — heap alloc
int unboxed = (int)boxed; // unboxing
Answer
A ref struct is guaranteed to stay on the stack — the compiler prevents it escaping to the heap. That makes it safe
to wrap a raw memory pointer, which is exactly what Span<T> does.
Restrictions: can't be boxed, can't be a generic type argument, can't be a field on a regular class, can't be used in
async methods or capturing lambdas.
Example
Answer
?? returns the left side if it isn't null, otherwise the right side — a safe default.
?. short-circuits the entire member-access chain the moment it hits null, returning null instead of throwing
NullReferenceException. Combine them for clean null-safe one-liners.
Example
Answer
The compiler rewrites foreach into a while loop that calls GetEnumerator(), then loops calling MoveNext() and
reading Current. If the enumerator implements IDisposable, a try/finally disposes it even if the body throws.
Example
Answer
An abstract class can hold state (fields), constructors, and fully implemented methods. A class can only inherit from
one.
An interface since C# 8 can have default method bodies but no instance state. A class can implement any number
of interfaces.
Use abstract class for shared logic to inherit. Use interface for a capability contract.
Example
abstract class Shape {
public abstract double Area();
public void Print() => [Link](Area());
}
interface IResizable { void Resize(double factor); }
Answer
Polymorphism means one variable behaves differently at runtime depending on the actual type of the object it
holds. Mark the base method virtual, override it in a subclass, and the runtime dispatches via the vtable — no
if/switch on the caller's side.
Example
class Animal {
public virtual string Sound() => "...";
}
class Dog : Animal {
public override string Sound() => "Woof";
}
Animal a = new Dog();
[Link]([Link]()); // "Woof" — runtime dispatch
Answer
A primary constructor puts parameters directly on the class or struct declaration. Those parameters are in scope
everywhere in the type body — no separate constructor block needed.
Example
Q11 What are init-only setters and what problem do they solve?
Answer
init setters let you assign a property during object initialiser syntax but block any further change afterwards. This
gives immutability without forcing everything through a constructor — ideal for DTOs.
Example
class Order {
public int Id { get; init; }
public string Product { get; init; }
}
var o = new Order { Id = 1, Product = "Book" }; // ok
[Link] = 2; // compile error — init-only
Q12 How does the runtime dispatch a virtual call vs. an interface call?
Answer
Virtual call: the object header points to its Method Table. The vtable slot for that method is at a fixed offset — one
pointer indirection, very fast.
Interface call: the runtime looks up the interface map inside the Method Table to find the slot — slightly more work,
though the JIT often inlines or caches it.
Example
Answer
A record is a reference type with compiler-generated value semantics: Equals, GetHashCode, == and != all
compare by property values rather than object identity. You also get a with-expression for non-destructive
mutation.
Example
Answer
Q15 When would you prefer a record struct over a record class?
Answer
When the object is small, short-lived, and in a hot path. record struct allocates on the stack — zero GC pressure.
Coordinate pairs, colour values, and price ranges are good candidates. You still get value equality and
with-expressions.
Example
Q16 What are generic constraints and what kinds are available?
Answer
Constraints tell the compiler what operations are legal on a type parameter.
where T : class -- reference type where T : struct -- value type where T : new() -- has parameterless constructor
where T : SomeBase -- inherits SomeBase where T : IFoo -- implements IFoo where T : notnull -- non-nullable
Example
T CreateAndLog<T>(ILogger log)
where T : class, new()
{
var obj = new T();
[Link]([Link]());
return obj;
}
Answer
out (covariance): type only flows OUT. IEnumerable<Dog> is assignable to IEnumerable<Animal> because you
only read from it.
in (contravariance): type only flows IN. Action<Animal> is assignable to Action<Dog> because a handler that
accepts any Animal can certainly handle a Dog.
Example
Answer
For reference types the JIT generates ONE shared native code path (all references are the same size). For value
types it generates a separate native code path per type — so List<int> and List<double> get different machine
code, avoiding boxing entirely.
Example
Ch 6 — Collections
Answer
IEnumerable<T> is the bare minimum: forward-only iteration. ICollection<T> adds Count, Add, Remove, Contains,
and Clear. Use IEnumerable when you only need to loop; ICollection when the caller needs to modify or measure.
Example
void PrintAll(IEnumerable<int> items) {
foreach (var x in items) [Link](x);
}
void AddIfMissing(ICollection<int> items, int val) {
if () [Link](val);
}
Answer
Answer
FrozenDictionary is a read-only dictionary optimised at construction time. Because it never changes, the runtime
can pick a perfect hash layout. For read-heavy lookup tables like routing tables or config maps — built once, read
millions of times — it is measurably faster.
Example
var config = new Dictionary<string,string> {
["host"] = "localhost", ["port"] = "5432"
}.ToFrozenDictionary();
Answer
It uses an array of buckets. The key's hash code selects a bucket. Collisions are handled by chaining. Average
lookup is O(1). A poor GetHashCode that returns the same value for many keys degrades to O(n) linear scan.
Example
Answer
It splits the bucket array into stripes and assigns one lock per stripe. Reads are mostly lock-free via volatile reads.
Writes lock only the stripe that the key hashes into, so two threads writing to different keys rarely contend.
Example
var cd = new ConcurrentDictionary<int, int>();
// Thread-safe increment — no external lock needed
[Link](1, addValue: 1,
updateValueFactory: (k, v) => v + 1);
Answer
A delegate is a type-safe function pointer — anyone holding it can invoke it directly. An event wraps that delegate
and restricts outside code to only += and -=. Only the declaring class can fire it. Events enforce the
publisher/subscriber pattern without letting subscribers call each other.
Example
class Button {
public event EventHandler Clicked;
public void SimulateClick() => Clicked?.Invoke(this, [Link]);
}
// Outside: [Link] += handler; -- ok
// Outside: [Link](this, e); -- compile error
Answer
A closure is a lambda that captures a variable from its enclosing scope. The compiler creates a hidden class to
hold it. That variable is shared — changes inside the lambda are visible outside. Classic bug: capturing a loop
variable directly means all lambdas share the same final value.
Example
var funcs = new List<Func<int>>();
for (int i = 0; i < 3; i++) {
int copy = i; // capture a fresh copy each iteration
[Link](() => copy);
}
// Without copy: all three lambdas return 3 (loop's final i)
Answer
A delegate is compiled native code you can call but not inspect. An expression tree is an object graph describing
the code — you can inspect every node, translate it (EF Core translates LINQ trees to SQL), or compile it to a
delegate at runtime.
Example
Q27 What is a multicast delegate and what happens when one handler throws?
Answer
A multicast delegate holds a list of methods and calls them in registration order. If one handler throws the
exception propagates immediately and remaining handlers never run. To guarantee all handlers run, iterate
GetInvocationList() manually with try/catch per call.
Example
// Safe:
foreach (Action h in [Link]())
try { h(); } catch { /* log */ }
Ch 8 — LINQ
Answer
IEnumerable runs the query in memory — data is fetched first, filtered in C#. IQueryable holds an expression tree;
the provider (EF Core) translates the whole query to SQL before any data crosses the wire. Always use IQueryable
against a database.
Example
Answer
A LINQ query doesn't run when you write it — it runs the moment you iterate it (ToList, First, foreach, etc.).
Changes to the source made after the query is written but before iteration are visible. Running the query twice hits
the source twice.
Example
Answer
IAsyncEnumerable<T> streams items one at a time from an async source without buffering everything in memory
first — think reading rows page by page. Consume with await foreach.
Example
Q31 How does the EF Core LINQ provider translate expression trees to SQL?
Answer
When you query a DbSet, EF Core receives an IQueryable with an expression tree. The query provider walks the
tree, maps C# constructs to SQL, and sends one SQL statement to the database. No filtering happens in C#.
Example
Answer
Count() scans every element to get the total. Any() stops at the first — O(1) for the question 'is there anything
here?'. On a million-item sequence Any() returns immediately; Count() scans the lot. Exception: List<T>.Count and
[Link] are O(1) properties.
Example
var items = GetMillionItems();
bool slow = [Link]() > 0; // scans everything
bool fast = [Link](); // stops at first element
Ch 9 — Pattern Matching
Answer
List patterns let you match the shape and content of an array or list in a switch expression. Pin specific positions,
use _ as a single-element wildcard, and .. for any-length slices.
Example
int[] nums = { 1, 2, 3, 4 };
var desc = nums switch {
Answer
No — it is purely a compile-time hint. At runtime string? and string are identical types. The annotations let the
compiler warn about potential null dereferences but insert no runtime checks. You still need explicit null guards for
runtime safety.
Example
#nullable enable
string? name = null;
[Link]([Link]); // compiler warning, but compiles
// -> NullReferenceException at runtime
Ch 11 — C# Error Handling
Q35 What is the difference between throw ex and throw inside a catch block?
Answer
throw ex resets the stack trace — you lose where the exception actually originated. Bare throw re-throws with the
original stack trace intact. Always use bare throw when you just want to rethrow after logging.
Example
Answer
Throwing is expensive: the runtime walks every stack frame to build the trace, allocates the exception object, and
unwinds the stack. On a hot path this is thousands of times slower than returning a result. Use exceptions for truly
unexpected failure, not control flow.
Example
Q37 How does the CLR unwind the stack when an exception is thrown?
Answer
Answer
Task always allocates a heap object. ValueTask is a struct — when the result is already ready (cache hit) it can
return with zero allocation. Use ValueTask on methods that are frequently synchronous. Rule: never await a
ValueTask more than once.
Example
Answer
By default await resumes on the captured SynchronizationContext (e.g., UI thread). ConfigureAwait(false) says
resume on any thread-pool thread. In library code this is a small win and prevents certain deadlocks. In UI event
handlers where you need to touch controls after the await, don't use it.
Example
Q40 What state machine does the compiler generate for an async method?
Answer
The compiler rewrites every async method into a struct implementing IAsyncStateMachine. Each await point
becomes a numbered state. When an await suspends, locals are saved and the method returns. When the awaited
task completes MoveNext() resumes from the saved state.
Example
// You write:
async Task<int> AddAsync(int a, int b) {
await [Link](1);
return a + b;
}
// Compiler generates a hidden struct:
// State 0 -> start delay, save a+b, return
Answer
SynchronizationContext controls how continuations are scheduled back to a specific thread. [Link] Core has no
SynchronizationContext, so await resumes on a free thread-pool thread.
Important: even in [Link] Core, blocking with .GetResult() or .Wait() wastes a thread-pool thread and can cause
starvation under load. Never do it in production.
Example
Q42 What is the difference between async void and async Task?
Answer
async Task lets callers await and catch exceptions. async void cannot be awaited — exceptions thrown inside
crash the process. The only accepted use of async void is event handlers where the signature is forced on you.
Example
Q43 Why does calling .Result on a Task cause a deadlock in some contexts?
Answer
The async method schedules its continuation to resume on the current SynchronizationContext thread. But .Result
blocks that same thread waiting for the Task. The continuation can never run — deadlock.
Example
// WinForms button click:
private void btn_Click(object s, EventArgs e)
{
var data = FetchDataAsync().Result; // deadlock!
// FetchDataAsync wants the UI thread back,
// but the UI thread is blocked right here.
}
// Fix: make the handler async and await
Answer
lock is syntax sugar for [Link] and [Link] wrapped in try/finally, ensuring the lock is always released.
Always lock on a private readonly object — never on this or a public type.
Example
// Compiles to:
bool taken = false;
[Link](_sync, ref taken);
try { _count++; }
finally { if (taken) [Link](_sync); }
Answer
[Link] is a dedicated lock type. The compiler recognises it and emits more optimised primitives
than generic [Link]/Exit. You still use the same lock keyword.
Example
Answer
Channel<T> is async-first — both sides use WriteAsync/ReadAsync, no thread is ever blocked. BlockingCollection
blocks a real thread while waiting, wasting thread-pool resources. In [Link] Core background services always
prefer Channel<T>.
Example
var ch = [Link]<string>();
// Producer
await [Link]("hello");
// Consumer
await foreach (var msg in [Link]())
[Link](msg);
Answer
When too many threads are blocked (usually from .Result on async code), the pool queue fills but has no free
threads to drain it. The pool injects new threads slowly — one per second — causing latency spikes. Diagnose with
dotnet-counters watching [Link] length.
Example
Answer
Gen 0 is collected most often and holds new short-lived objects. Survivors are promoted to Gen 1, then to Gen 2
which is collected rarely and holds long-lived objects like static caches. The insight: most objects die young, so
collecting Gen 0 alone clears most garbage cheaply.
Example
Q49 What is the Large Object Heap (LOH) and why does it matter?
Answer
Objects >= 85,000 bytes go straight to the LOH. The LOH is only collected during Gen 2 collections (rare) and isn't
compacted by default, so it fragments. Fix: rent large buffers from ArrayPool instead of allocating fresh each time.
Example
// Bad -- 100 KB on the LOH every call
byte[] buf = new byte[100_000];
Answer
Workstation GC (default on desktop) runs on the application thread, keeping pauses short. Server GC (default in
[Link] Core) creates a separate heap and GC thread per logical CPU, giving much higher throughput on
multi-core machines at the cost of more memory.
Example
Q51 What is the dispose pattern and why does Dispose(bool) have a parameter?
Answer
The parameter separates managed cleanup (run only from Dispose) from unmanaged cleanup (run from both
Dispose and finalizer). When true you were called from Dispose() — safe to release managed objects too. When
false you were called from the finalizer — only release unmanaged handles.
Example
class ResourceHolder : IDisposable {
private bool _disposed;
protected virtual void Dispose(bool disposing) {
if (_disposed) return;
if (disposing) _managed?.Dispose(); // managed
_nativeHandle.Free(); // unmanaged
_disposed = true;
}
public void Dispose() { Dispose(true); [Link](this); }
~ResourceHolder() { Dispose(false); }
Answer
1. Watch process memory with dotnet-counters or Prometheus. 2. Capture a dump with dotnet-dump or ProcDump
when memory is high. 3. Analyse with dotnet-dump analyze, Visual Studio, or WinDbg+sos. 4. Run dumpheap
-stat to spot types with unexpectedly high instance counts.
Common culprits: static collections growing forever, event subscriptions never removed, IDisposable not disposed.
Example
Q53 What is the performance cost of reflection and how do you reduce it?
Answer
Reflection bypasses JIT optimisations and does dynamic dispatch — 10-100x slower than direct calls. Cache
MethodInfo/PropertyInfo at startup so you pay the lookup cost once. Better: compile the reflected call to a delegate
once — then invocation is near-native.
Example
Answer
Source generators run at compile time and emit C# code. The result is strongly typed, AOT-compatible, and has
zero runtime reflection. [Link] source-gen mode and Mapperly both use this. Trade-off: more complex
build setup, but huge runtime gains.
Example
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }
// Reflection-free at runtime:
var json = [Link](order, [Link]);
Answer
Span<T> is a stack-only struct that references a contiguous memory block — array slice, stack memory, or
unmanaged memory — without copying. Parsing, slicing, and in-place processing become zero-allocation
operations.
Answer
Span<T> is a ref struct — it must live on the stack. A class lives on the heap. Storing a Span inside a class would
let it outlive the stack memory it points to — a use-after-free. Use Memory<T> instead: it lives on the heap and
converts to Span on demand.
Example
class Buffer {
// Span<byte> _data; // compile error
Memory<byte> _data; // heap-safe
public Span<byte> AsSpan() => _data.Span;
}
Answer
ArrayPool<T>.Shared rents arrays from a pre-allocated pool. Return them when done and the memory is reused.
Use it whenever you need a large temporary buffer — network reads, serialisation — to avoid LOH allocations.
Example
Ch 17 — Serialization
Answer
It generates all serialisation/deserialisation code at compile time — no runtime reflection. Use it for Native AOT
(where reflection-based serialisation doesn't work) or high-throughput hot paths.
Example
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
public partial class MyContext : JsonSerializerContext { }
Answer
BinaryFormatter can execute arbitrary code during deserialisation — a well-known attack vector. It's also brittle for
versioning as it couples serialised bytes to internal type layout. Use [Link] for JSON, MessagePack for
binary, or Protobuf for cross-language binary.
Example
// Throws in .NET 9+
// [Link](stream, obj);
Ch 18 — C# Version Features
Answer
field gives access to the compiler-generated backing field inside a property accessor without declaring the field
yourself. Useful for semi-auto properties — e.g., trim on get, null-check on set.
Example
Answer
Answer
On first call a method is JIT-compiled at Tier 0 — fast compile, fewer optimisations. After enough calls the runtime
recompiles it at Tier 1 with full optimisations. Fast startup (cheap Tier 0) plus fast steady-state (optimised Tier 1)
with no code changes needed.
Example
Answer
Native AOT compiles the whole app to a standalone native binary — no JIT, no CLR bundled. Benefits: tiny
startup, lower memory, single-file. Costs: no runtime code generation, limited dynamic features, longer build times,
trimming can break libraries that use reflection.
Answer
PGO feeds runtime profiling data back into the JIT — which branches are taken, which types appear — to produce
better-optimised code. Dynamic PGO does this automatically at runtime. On by default since .NET 8; gives
meaningful throughput gains with zero code changes.
Example
Answer
Stopwatch doesn't account for JIT warm-up, CPU frequency scaling, GC pauses, or process noise. Your first
measurement includes JIT compilation time. BenchmarkDotNet handles warm-up, statistical analysis, multiple
iterations, GC pressure, and hardware counters.
Example
[MemoryDiagnoser]
public class MyBench {
[Benchmark] public string Concat() => "Hello" + " " + "World";
[Benchmark] public string Interpolate() => $"Hello World";
}
[Link]<MyBench>();
Q66 What does MemoryDiagnoser show and how do you read Gen columns?
Answer
Adds Allocated (bytes per op) and Gen0/Gen1/Gen2 (collections per 1,000 ops). Gen0=1 means one Gen 0
collection per 1,000 ops — usually fine. Any non-zero Gen2 means long-lived allocations in the expensive heap —
investigate.
Example
// BenchmarkDotNet output:
// | Method | Mean | Gen0 | Allocated |
// |--------|--------|--------|-----------|
// | Alloc | 120 ns | 0.0234 | 96 B |
// ~23 Gen0 collections per 1,000 ops
Ch 21 — BCL and IO
Answer
Answer
RandomAccess provides static methods for reading/writing files at specific byte offsets without seeking. Multiple
threads can safely read different parts of a large file concurrently — something FileStream can't do safely with its
shared seek position.
Example
Ch 22 — NuGet
Answer
Central Package Management lets you declare every package version in one file at the repo root. Individual .csproj
files reference packages without specifying a version — the central file supplies it, eliminating version drift across a
large monorepo.
Example
<!-- [Link] -->
<Project>
<ItemGroup>
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Dapper" Version="2.1.28" />
</ItemGroup>
</Project>
Answer
MSBuild automatically imports this file from any ancestor directory. Put shared settings — Nullable,
TreatWarningsAsErrors, TargetFramework — here once and they apply to every project under that folder. No more
copying the same boilerplate into each .csproj.
Example
<!-- [Link] at repo root -->
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
Answer
Multi-targeting builds one project against multiple framework versions in one pass, producing separate binaries for
each. Use it in library projects that need to support both net8.0 and net9.0, or where you want to use newer APIs
on newer runtimes with a fallback.
Example
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
#if NET9_0
// use net9-only API
#else
// fallback
#endif
Ch 24 — .NET CLI
Q72 What is the difference between dotnet build and dotnet publish?
Answer
dotnet build compiles code and resolves NuGet references — output is not self-contained. dotnet publish produces
a ready-to-deploy folder with runtime files, static content, and any trim/single-file options you configure.
Example
Answer
[Link] pins the exact .NET SDK version for a directory tree. Without it, dotnet commands use the latest
installed SDK — which silently breaks builds when teammates or CI have different versions.
Example
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestPatch"
}
}
Answer
TaskCompletionSource<T> creates a Task you control manually — you decide when to set the result, throw an
exception, or cancel it. Classic use: wrapping an old callback-based API into an awaitable Task.
Example
Answer
[Link] waits for ALL tasks to finish and collects all results. If any faults, the combined task faults.
[Link] completes as soon as the FIRST task finishes — useful for timeout patterns or racing two sources.
Example
// WhenAll -- parallel fetch, correct syntax
var usersTask = GetUsersAsync();
var ordersTask = GetOrdersAsync();
await [Link](usersTask, ordersTask);
var users = await usersTask; // already complete
var orders = await ordersTask;
Answer
Empty Web -- bare canvas, no scaffolding. Web API (controller-based) -- MVC controllers returning JSON. Minimal
API -- direct endpoint registration, no controllers. MVC with Views -- server-rendered HTML via Razor views. Razor
Pages -- one page-model file per page. Blazor -- component-based UI (Server or WebAssembly). gRPC Service --
Protobuf-based typed RPC.
Example
Q77 When would you choose Minimal APIs over controller-based APIs?
Answer
Minimal APIs shine for microservices, small focused services, and serverless functions where you want the least
ceremony possible. Controller-based APIs are better for larger teams wanting conventions, area support, a full filter
pipeline, and easy discoverability across a big codebase.
Example
Ch 26 — Application Host
Answer
WebApplicationBuilder merges HostBuilder and WebHostBuilder into one fluent API. Register services on
[Link], configure the host on [Link], call [Link]() to get the WebApplication where you
add middleware and map routes.
Example
Answer
IHostedService ties background work to the application lifetime. StartAsync fires at host start; StopAsync on
graceful shutdown. Typical uses: message queue consumers, periodic cleanup jobs, warming up caches, seeding
data.
Example
Answer
StopAsync cancels the host lifetime CancellationToken. Every registered [Link] is called with
a deadline (default 5 seconds). After that the host terminates everything regardless.
Example
Ch 27 — Controllers
Answer
Model binding reads incoming data from route segments, query string, body, form fields, and headers, and maps
them to action method parameters. The framework tries each source in a default order unless you pin one with an
attribute.
Example
[HttpGet("{id}")]
public IActionResult Get(
int id, // from route
string filter, // from query string
[FromHeader] string token) // from header
{ ... }
Answer
[FromBody] -- deserialises the request body (JSON/XML), read once. [FromQuery] -- reads a query-string
parameter (?key=value). [FromRoute] -- reads a route template segment ({id}). [FromForm] -- reads a
multipart/form-data or urlencoded form field.
Example
[HttpPost("upload")]
public IActionResult Upload(
[FromRoute] int tenantId,
[FromQuery] string tag,
[FromForm] IFormFile file)
{ ... }
Answer
Answer
Controllers are Transient by default — created fresh per request with all dependencies injected, then discarded
when the request ends. You can change this by registering controllers as services, but Transient is the safe
default.
Example
public class OrdersController : ControllerBase {
private readonly IOrderService _svc;
public OrdersController(IOrderService svc) => _svc = svc;
// Fresh _svc instance every request
}
Answer
Three main options: 1. UseExceptionHandler middleware -- catches anything unhandled, redirects to error
endpoint. 2. IExceptionHandler (.NET 8+) -- clean interface, chain multiple handlers, first match wins. 3. Exception
filter -- runs inside the MVC pipeline, has access to action context.
Example
public class GlobalExceptionHandler : IExceptionHandler {
public async ValueTask<bool> TryHandleAsync(
HttpContext ctx, Exception ex, CancellationToken ct)
{
[Link] = 500;
await [Link](new { error = [Link] });
return true;
}
}
[Link]<GlobalExceptionHandler>();
[Link]();
Ch 28 — Minimal APIs
Answer
Minimal APIs register endpoints directly in [Link] with MapGet/MapPost etc. No controller class, no
[ApiController], no action conventions. They use the same underlying pipeline but with far less ceremony.
Example
[Link]("/users/{id}", async (int id, IUserService svc) => {
var user = await [Link](id);
return user is null ? [Link]() : [Link](user);
});
Answer
Add UseAuthentication() and UseAuthorization() to the pipeline, then chain RequireAuthorization() onto individual
endpoints or groups.
Example
[Link]();
[Link]();
Answer
Use the [Link] package. Create an ApiVersionSet and attach endpoints to specific versions using
WithApiVersionSet and MapToApiVersion.
Example
var vs = [Link]()
.HasApiVersion(new ApiVersion(1))
.HasApiVersion(new ApiVersion(2))
.Build();
[Link]("/orders", GetV1).WithApiVersionSet(vs).MapToApiVersion(1);
[Link]("/orders", GetV2).WithApiVersionSet(vs).MapToApiVersion(2);
Answer
Minimal APIs support IEndpointFilter added with AddEndpointFilter. Filters form a pipeline around the endpoint
handler — useful for logging, validation, and auth checks. Traditional MVC action/resource filters don't apply here.
Example
[Link]("/orders", CreateOrder)
.AddEndpointFilter(async (ctx, next) => {
// before handler
var result = await next(ctx);
// after handler
return result;
});
Q90 What are the trade-offs of Minimal APIs in large enterprise apps?
Answer
Pros: less boilerplate, faster startup, great for microservices and vertical-slice architecture. Cons: no
convention-based routing, no area support, harder to discover endpoints across a big codebase, simpler filter
pipeline than full MVC.
Example
// Keep large Minimal API apps organized:
public static class OrderEndpoints {
public static void Map(IEndpointRouteBuilder app) {
[Link]("/orders", GetAll);
[Link]("/orders", Create);
}
}
Q91 What is the order of middleware execution and why does it matter?
Answer
Middleware runs in registration order for the request and in reverse order for the response. Order matters because
each middleware decides whether to call next(). UseAuthentication must come before UseAuthorization —
establish identity before checking permissions.
Example
Q92 What are all the ways to create middleware in [Link] Core?
Answer
1. Inline lambda: [Link]((ctx, next) => ...) 2. Convention-based class with InvokeAsync (singleton lifecycle) 3.
Factory-based: implement IMiddleware, created per request via DI 4. Terminal: [Link]() -- no next call 5.
Registration: [Link]<T>()
Example
Answer
Convention-based is instantiated once at startup — effectively a singleton. Don't inject scoped services via its
constructor. Factory-based (IMiddleware) is resolved from DI on every request, so injecting scoped services in the
constructor is safe.
Example
Answer
Register UseExceptionHandler before everything else. It catches any unhandled exception, resets the response,
and runs an error handler that writes the error response.
Example
[Link](errApp => {
[Link](async ctx => {
var ex = [Link]<IExceptionHandlerFeature>()?.Error;
Ch 30 — Filters
Answer
Authorization filters -- run first; short-circuit if not allowed. Resource filters -- run after auth but before model
binding; can return cached response early. Action filters -- run around the action method after model binding.
Exception filters -- catch exceptions from action methods. Result filters -- run around IActionResult execution.
Example
Answer
Implement IOrderedFilter and set the Order property. Lower numbers run earlier in the before-phase and later in
the after-phase (outermost wrapper). You can also set order when registering global filters.
Example
Q97 What is the difference between resource filters and action filters?
Answer
Resource filters fire before model binding — return a cached response without the framework ever reading the
body. Action filters fire after model binding — arguments are already populated, so you can inspect or mutate
them.
Example
Answer
Global filters apply to every action. Controller-level apply to all actions in that controller. Action-level apply to one
action. When Order values tie, global runs outermost, then controller, then action.
Example
Ch 31 — REST Fundamentals
Answer
Level 0 -- single URL, single verb (RPC over HTTP). Level 1 -- separate URLs per resource. Level 2 -- correct
HTTP verbs and proper status codes. Level 3 -- HATEOAS: responses include links to next possible actions. Most
real-world REST APIs target Level 2.
Example
// Level 2:
GET /orders -> 200 list
GET /orders/1 -> 200 single
POST /orders -> 201 created
PUT /orders/1 -> 200 updated
DELETE /orders/1 -> 204 no content
Answer
1. Client-Server -- independent evolution of client and server. 2. Stateless -- each request contains all context the
server needs; no server-side session. 3. Cacheable -- responses declare whether they can be cached. 4. Uniform
Interface -- resource identified by URI, self-descriptive messages. 5. Layered System -- client can't tell if talking
directly to server or proxy. 6. Code on Demand (optional) -- server can send executable code to client.
Example
// Stateless in practice:
// Bad: store user context in server-side session
// Good: send JWT in every request
Answer
PUT replaces the entire resource — any omitted field gets null or its default. PATCH applies a partial update —
only the fields you send are changed.
Example
Answer
POST /orders // each call creates a new order -> NOT idempotent
Answer
401 means no valid credentials — you aren't authenticated yet. 403 means credentials are valid but you don't have
permission for this resource — authenticated but not authorised.
Example
// 401: missing or expired token
GET /admin/reports
Authorization: Bearer expired_token -> 401
Answer
HATEOAS (Hypermedia As The Engine Of Application State) means responses include links describing what
actions the client can take next. The client discovers the API dynamically — no hard-coded URLs. This is REST
Level 3.
Example
{
"id": 1, "status": "Pending",
"_links": {
"pay": { "href": "/orders/1/pay", "method": "POST" },
"cancel": { "href": "/orders/1/cancel", "method": "DELETE" }
}
}
Answer
Offset pagination (?page=2&pageSize=20) is simple but slow for large offsets. Cursor/keyset pagination
(?after=lastId&pageSize=20) uses an index seek — fast and stable for live data. Link header pagination includes
prev/next URLs in response headers (GitHub style).
Example
GET /orders?after=550&pageSize=20
{
"data": [...],
"nextCursor": "570",
"hasMore": true
}
Answer
GET /users?fields=id,email
[
{ "id": 1, "email": "alice@[Link]" },
{ "id": 2, "email": "bob@[Link]" }
]
Ch 32 — API Versioning
Answer
URL (/api/v1/orders) -- most visible, easy to test in a browser. Query string (?api-version=1.0) -- clean URL, version
is optional. Header (api-version: 1.0) -- URL stays clean, needs tool support to discover. Content negotiation
(Accept: application/json;v=1.0) -- fully RESTful but complex.
Example
[Link](o => {
[Link] = [Link](
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("api-version"));
});
Answer
Call HasDeprecatedApiVersion() when building the ApiVersionSet. The framework adds a Deprecation response
header and marks the version as deprecated in generated OpenAPI docs.
Example
var vs = [Link]()
.HasApiVersion(new ApiVersion(2))
.HasDeprecatedApiVersion(new ApiVersion(1))
.Build();
Answer
Never remove or rename fields in an existing version. Only add new fields in new versions. Keep old endpoints live
until all consumers have migrated. Use Sunset headers to give advance notice of retirement.
Example
// V1 -- frozen forever
{ "id": 1, "name": "Alice" }
// V2 -- additive only
{ "id": 1, "name": "Alice", "email": "alice@[Link]" }
Ch 33 — Validation
Answer
Answer
Use SetValidator to delegate to a child validator for a nested object. Use RuleForEach to run a validator against
every item in a collection.
Example
Answer
Use MustAsync in the rule builder. Always call ValidateAsync on the validator — the synchronous Validate() simply
skips async rules.
Example
Answer
Register the validator in DI and accept services through its constructor. Use AddFluentValidationAutoValidation()
so [Link] Core runs the validator automatically on model binding.
Example
Answer
Ch 34 — Mapping
Answer
AutoMapper maps properties by convention at runtime using reflection. Common problems: silent mismatches
when property names differ, hard-to-trace global configuration, N+1 queries when mapping inside LINQ instead of
projecting at the DB, and reflection overhead on hot paths.
Example
Answer
Mapperly is a source-generator mapper — it generates the full mapping code at compile time from a partial class
you annotate. Zero runtime reflection, AOT-safe, and the generated code is human-readable.
Example
[Mapper]
public partial class OrderMapper {
public partial OrderDto ToDto(Order order);
}
// Compile-time generated:
// public partial OrderDto ToDto(Order o)
// => new OrderDto { Id = [Link], ... };
Answer
Mapperly: compile-time errors if a mapping is wrong, no reflection, AOT-safe, faster. AutoMapper: more flexible
conventions, easier for large legacy codebases, but runtime-only errors and reflection-based.
Example
Answer
When the mapping has non-trivial business logic — conditional fields, computed values, lookups. Manual mapping
via extension methods is explicit, zero-dependency, and instantly debuggable.
Example
public static OrderDto ToDto(this Order o) => new() {
Id = [Link],
Total = [Link](i => [Link] * [Link]),
StatusLabel = [Link] == [Link] ? "Paid" : "Pending"
Q119 What is the options pattern and why prefer it over IConfiguration?
Answer
The options pattern binds a config section to a typed POCO. You inject IOptions<T>, IOptionsSnapshot<T>, or
IOptionsMonitor<T> instead of raw IConfiguration strings. Benefits: strongly typed, validated at startup, reloadable,
and easy to unit test.
Example
public class SmtpOptions {
public string Host { get; set; } = "";
public int Port { get; set; } = 587;
}
[Link]<SmtpOptions>(
[Link]("Smtp"));
Answer
IOptions<T> -- singleton; values fixed at startup. Simplest option. IOptionsSnapshot<T> -- scoped; re-reads config
once per HTTP request. IOptionsMonitor<T> -- singleton with OnChange callback; notified immediately when
config changes. Use in background services.
Example
Answer
Call ValidateDataAnnotations() and ValidateOnStart(). The framework validates the bound POCO at startup and
throws immediately if any annotations are violated — fail fast.
Example
Answer
Dev: User Secrets (dotnet user-secrets) -- stored outside the repo. Production: environment variables, Azure Key
Vault, AWS Secrets Manager, or HashiCorp Vault. The config system layers multiple providers, so secrets override
Ch 36 — Error Handling
Answer
UseExceptionHandler wraps the rest of the pipeline in a try/catch. When an unhandled exception escapes it resets
the response and runs an error handler. Register it first so it catches errors from all other middleware.
Example
[Link]("/error");
[Link]("/error", (HttpContext ctx) => {
var ex = [Link]<IExceptionHandlerFeature>()?.Error;
return [Link](detail: ex?.Message, statusCode: 500);
});
Answer
A clean interface — implement TryHandleAsync, return true if you handled it, false to pass to the next handler.
Chain multiple handlers for different exception types. Integrates nicely with Problem Details.
Example
public class NotFoundHandler : IExceptionHandler {
public async ValueTask<bool> TryHandleAsync(
HttpContext ctx, Exception ex, CancellationToken ct)
{
if (ex is not NotFoundException) return false;
[Link] = 404;
await [Link](
new ProblemDetails { Title = "Not found", Status = 404 });
return true;
}
}
Q125 How do you return Problem Details (RFC 9457) in [Link] Core?
Answer
Call AddProblemDetails() and UseExceptionHandler(). [Link] Core returns ProblemDetails JSON for 4xx/5xx
responses automatically. Return [Link]() from Minimal APIs or Problem() from controllers for manual
control.
Example
[Link]();
// Manual:
return [Link](
title: "Validation failed",
statusCode: 422,
detail: "Quantity must be positive");
Answer
Use IExceptionHandler with a switch expression to map known domain exception types to status codes.
Example
Ch 37 — Logging
Answer
Structured logging stores log entries as key-value pairs rather than plain strings. When you pass a named
placeholder like {OrderId}, the logging framework saves the value as a separate field. Log aggregators like Seq or
Elasticsearch can then filter on orderId == 1234 instead of doing regex on a text blob.
Example
// Structured -- OrderId stored as searchable field
_logger.LogInformation("Order {OrderId} shipped to {City}",
[Link], [Link]);
Q128 What are logging scopes and when do you use them?
Answer
A logging scope attaches extra key-value pairs to all log entries made within a using block. It is the right way to add
RequestId, UserId, or TenantId to every line inside an operation without passing those values to every method.
Example
Answer
Set the EF Core command log level to Information in your logging config. For development also call
EnableSensitiveDataLogging() to include parameter values — never use that in production.
Example
{
"Logging": { "LogLevel": {
"[Link]": "Information"
}}
}
// Dev only:
[Link]().LogTo([Link]);
Answer
Typical stack: Serilog as the library, structured JSON sinks (Seq locally, Elasticsearch/Loki/CloudWatch in cloud),
correlation IDs on every entry, and alerts in Grafana or Kibana on elevated error rates.
Example
[Link] = new LoggerConfiguration()
.[Link]()
.[Link]()
.[Link](new JsonFormatter())
.[Link]("[Link]
.CreateLogger();
[Link]();
Answer
Use [Link] with rollingInterval and fileSizeLimitBytes. retainedFileCountLimit controls how many old files to
keep before Serilog deletes the oldest.
Example
[Link] = new LoggerConfiguration()
.[Link]("logs/[Link]",
rollingInterval: [Link],
fileSizeLimitBytes: 50_000_000,
retainedFileCountLimit: 14,
rollOnFileSizeLimit: true)
.CreateLogger();
Ch 38 — Health Checks
Q132 What is the difference between a liveness probe and a readiness probe?
Answer
Liveness: is the process alive? If not, Kubernetes restarts the container. Readiness: is the app ready to serve
traffic? If not, Kubernetes removes the pod from load balancer rotation but does not restart it. A starting app should
fail readiness but pass liveness.
Example
// Liveness -- just 'am I alive?'
[Link]("/health/live", new() { Predicate = _ => false });
// Readiness -- run all registered checks
Answer
Ch 39 — Dependency Injection
Answer
Transient -- new instance every time requested. Use for stateless services. Scoped -- one instance per HTTP
request (or per DI scope). DbContext is the classic example. Singleton -- one instance for the entire app lifetime.
Use for caches, configuration, HttpClientFactory.
Example
[Link]<IEmailSender, SmtpEmailSender>();
[Link]<IOrderRepository, EfOrderRepository>();
[Link]<ICache, MemoryCache>();
Answer
A captive dependency happens when a singleton captures a scoped service in its constructor. The scoped service
is supposed to be created and discarded per request, but being held by a singleton means it lives forever.
[Link] Core throws InvalidOperationException at startup when it detects this.
Example
Answer
Inject IServiceScopeFactory into the singleton or background service. Create a new scope around each unit of
work and resolve the scoped service from that scope.
Answer
Circular dependencies usually signal a design problem. Cleanest fix: extract a third class both can depend on, or
use events/mediator to decouple them. Quick fix: Lazy<T> breaks the cycle by deferring resolution until first use.
Example
public class ServiceA {
private readonly Lazy<IServiceB> _b;
public ServiceA(Lazy<IServiceB> b) => _b = b;
public void DoWork() => _b.[Link]();
}
Answer
Register all implementations, inject IEnumerable<T>, and pick by a key property at runtime. Alternatively, use a
factory delegate.
Example
[Link]<IPaymentGateway, StripeGateway>();
[Link]<IPaymentGateway, PayPalGateway>();
Answer
DbContext is the unit of work: tracks changes, manages identity map, coordinates SaveChanges. DbSet<T>
represents one table. You write LINQ against DbSet; DbContext translates and executes them.
Example
Answer
Eager (Include) -- loads related entities in the same query via JOIN. No extra round-trips. Lazy -- related entities
load automatically when you access the navigation property (requires proxies). Risk: N+1 queries. Explicit -- you
call LoadAsync manually when you decide you need the related data.
Example
// Eager
var orders = await [Link](o => [Link]).ToListAsync();
Answer
Use it for read-only queries — GET endpoints, reports — where you won't update the returned entities. EF Core
skips adding them to the change tracker, reducing both memory and CPU overhead.
Example
Answer
When you Include multiple collection navigations EF Core generates a cartesian-product JOIN. The result set
explodes: 1 order x 100 items x 50 tags = 5,000 rows. AsSplitQuery fires a separate SQL query per collection,
avoiding the explosion at the cost of extra round-trips.
Example
Answer
AddDbContextPool reuses DbContext instances across requests instead of creating and GC-ing one per request.
Between uses the context is reset — change tracker cleared. This cuts GC pressure significantly on
high-throughput services.
Example
[Link]<AppDbContext>(
o => [Link](connectionString),
poolSize: 128);
Answer
Answer
EF Core 7 added ExecuteUpdateAsync and ExecuteDeleteAsync. These emit a single UPDATE or DELETE SQL
statement directly — no entities loaded, no change tracking, no SaveChanges needed.
Example
await [Link]
.Where(o => [Link] == "Cancelled"
&& [Link] < [Link](-1))
.ExecuteDeleteAsync();
await [Link]
.Where(p => [Link] == "Electronics")
.ExecuteUpdateAsync(s => [Link](p => [Link], 0.1m));
Answer
Authentication answers WHO ARE YOU -- verifying identity (JWT, cookie, API key). Authorization answers WHAT
CAN YOU DO -- checking what the authenticated identity is allowed to do. The two middleware must be registered
in that order: authenticate first, then authorise.
Example
[Authorize(Roles = "Admin")]
public IActionResult DeleteUser(int id) { ... }
Q147 What is the difference between cookie auth and JWT bearer?
Answer
Cookie auth stores the session token in an HTTP-only cookie -- the browser attaches it automatically. Ideal for
server-rendered web apps. JWT bearer puts the token in the Authorization header -- the client must manage it.
Ideal for SPAs, mobile, and service-to-service calls.
Example
// Cookie
[Link](
[Link]).AddCookie();
// JWT Bearer
[Link](
[Link])
.AddJwtBearer(o => [Link] = "[Link]
Q148 What are refresh tokens and how does the JWT refresh flow work?
Answer
Access tokens are short-lived (minutes). Refresh tokens are long-lived (days/weeks). Flow: 1. Login -> server
returns access token + refresh token. 2. Access token expires -> client POSTs refresh token to /auth/refresh. 3.
Server validates and returns new tokens. 4. On logout -> server revokes the refresh token.
Example
Answer
Define a named policy with AddAuthorization, specify what claims or roles are required, apply the policy name with
[Authorize(Policy='name')].
Example
[Link](o =>
[Link]("CanApproveOrders",
[Authorize(Policy = "CanApproveOrders")]
public IActionResult Approve(int id) { ... }
Answer
A Requirement is a plain marker class carrying data the authorization decision needs. A separate
AuthorizationHandler reads it and calls [Link] or [Link].
Example
public class MinimumAgeRequirement : IAuthorizationRequirement {
public int MinAge { get; }
public MinimumAgeRequirement(int min) => MinAge = min;
}
public class AgeHandler : AuthorizationHandler<MinimumAgeRequirement> {
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext ctx, MinimumAgeRequirement req)
{
var dob = [Link]("dob");
if (dob != null && CalculateAge([Link]) >= [Link])
[Link](req);
return [Link];
}
}
Answer
[Link]<IAuthorizationHandler, AgeHandler>();
[Link](o =>
[Link]("Over18",
p => [Link](new MinimumAgeRequirement(18))));
Q152 When is claims transformation necessary and how do you implement it?
Answer
JWT tokens carry static claims baked in at login. If the user's role changes after login the token doesn't reflect it
until expiry. IClaimsTransformation lets you augment claims on every request from the database.
Example
public class PermissionsTransformer : IClaimsTransformation {
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal p) {
var userId = [Link]("sub")?.Value;
var perms = await _db.GetPermissionsAsync(userId);
var id = new ClaimsIdentity();
foreach (var perm in perms)
[Link](new Claim("permission", perm));
[Link](id);
return p;
}
}
Answer
Answer
Enable TwoFactorEnabled on the user, configure a token provider, use GenerateTwoFactorTokenAsync to create
a code, and TwoFactorSignInAsync to validate it.
Example
var token = await _userManager
.GenerateTwoFactorTokenAsync(user, "Email");
await _emailSender.SendAsync([Link], "Your code", token);
Q155 How do you lock out users after failed login attempts?
Answer
Enable lockout in IdentityOptions. Every failed PasswordSignInAsync call increments the failure counter. After
MaxFailedAccessAttempts the account is locked for DefaultLockoutTimeSpan.
Example
[Link] = 5;
[Link] = [Link](15);
[Link] = true;
// PasswordSignInAsync handles lockout automatically
Answer
After a successful Identity login, generate a JWT containing the user's claims with JwtSecurityTokenHandler.
Return it to the client. Configure AddJwtBearer on the API to validate incoming tokens.
Example
Answer
Implement IUserStore<T> plus whichever optional interfaces you need (IUserPasswordStore, IUserRoleStore).
Register with AddIdentityCore().AddUserStore<T>(). UserManager delegates all persistence to your store.
Example
Answer
CORS controls which origins browsers allow to call your API. Allowing * combined with cookie auth is dangerous:
any website can make credentialed requests on behalf of your users. Always restrict origins, methods, and
headers to what you need.
Example
Answer
Before a cross-origin request with custom headers or non-simple verbs, the browser sends an OPTIONS preflight.
The server must reply with Access-Control-Allow-* headers. If the preflight fails the browser blocks the real
request. [Link] Core handles this automatically when CORS middleware is configured.
Example
OPTIONS /api/orders
Origin: [Link]
Access-Control-Request-Method: POST
// Server replies:
Access-Control-Allow-Origin: [Link]
Access-Control-Allow-Methods: POST
Answer
[Link]([Link])
.AddJwtBearer(o => {
Answer
If stolen, the attacker gets long-lived access. Mitigations: store only a hashed copy in the database, rotate on every
use, bind to device fingerprint, set expiry, and revoke all tokens for a user on logout or password change.
Example
Answer
Store refresh tokens in the database with an IsRevoked flag. On logout or password change, mark all of that user's
tokens as revoked. JWT access tokens can't be individually invalidated before expiry — the server-side store is the
only way.
Example
await _db.RefreshTokens
.Where(t => [Link] == userId)
.ExecuteUpdateAsync(s => [Link](t => [Link], true));
Answer
OAuth 2.0 is a delegation framework. A user grants a client app limited access to their resources on a server,
mediated by an Authorization Server, without handing the client their credentials. The client gets a scoped access
token.
Example
Q164 What is OpenID Connect and how does it differ from OAuth 2.0?
Answer
OAuth 2.0 is about authorization (access tokens). OpenID Connect adds authentication on top -- it returns an ID
token (JWT) saying WHO the user is (name, email, subject). Use OAuth 2.0 for API access delegation. Use OIDC
{
"access_token": "...",
"id_token": "<JWT: sub, email, name>",
"token_type": "Bearer"
}
// access_token -> what you can do
// id_token -> who you are
Ch 44 — Caching
Answer
IMemoryCache is in-process -- fast but private to one instance and lost on restart. IDistributedCache is backed by
Redis, SQL Server, etc. -- shared across all instances and survives restarts. Use memory cache for
single-instance; distributed cache for multi-instance deployments.
Example
// In-memory only
_memCache.Set("key", value, [Link](5));
Answer
Set TTL in DistributedCacheEntryOptions when writing. Configure Redis maxmemory-policy at the server level
(allkeys-lru is a sensible default) for what gets evicted when memory fills up.
Example
await _cache.SetStringAsync("product:1", json,
new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = [Link](1),
SlidingExpiration = [Link](20)
});
Answer
OutputCache stores the complete HTTP response for an endpoint and serves it directly to subsequent matching
requests -- the action never runs again until the entry expires. Register AddOutputCache and UseOutputCache,
annotate endpoints with CacheOutput.
Example
[Link]();
[Link]();
[Link]("/products", GetProducts)
.CacheOutput(p => [Link]([Link](10)));
Answer
Use VaryByQuery, VaryByHeader, or VaryByValue in the cache policy. This creates a separate cache entry per
unique combination of those values.
Example
[Link](o =>
[Link]("ByUser", p =>
[Link](ctx =>
[Link]("sub")?.Value ?? "anon")
.Expire([Link](30))));
Answer
HybridCache is a two-level cache: L1 is in-process memory for ultra-fast reads; L2 is a distributed cache (Redis).
On a hit it serves from L1. On a miss it checks L2, populates L1, and returns. Write-through keeps both in sync.
Example
[Link]();
Answer
[Link](
o => [Link] = "redis:6379");
[Link]();
// in-memory = L1, Redis = L2 automatically
Answer
FusionCache adds two extras HybridCache currently lacks: 1. Cache stampede protection -- only one request
fetches from source when cold; others wait. 2. Soft expiry / stale-while-revalidate -- serve a slightly stale value
while refreshing in the background. It also adds a circuit breaker for the distributed cache layer.
Example
[Link]()
.WithDefaultEntryOptions(o => o
.SetDuration([Link](5))
.SetFailSafe(true)); // serve stale if downstream is down
Ch 49 — Task Scheduling
Answer
BackgroundService is an abstract base class implementing IHostedService. Override ExecuteAsync and write your
work loop there. The host starts it on app start and cancels its CancellationToken on graceful shutdown.
Example
Answer
IHostedService is the low-level interface with StartAsync and StopAsync -- you manage everything.
BackgroundService is a convenience wrapper: it runs your ExecuteAsync in a Task and handles cancellation
wiring. Most of the time BackgroundService is all you need.
Example
Answer
By default (.NET 6+) the service stops but the host keeps running. Set BackgroundServiceExceptionBehavior to
StopHost to crash the whole app and let your process supervisor (Kubernetes, systemd) restart it.
Example
[Link]<HostOptions>(o =>
[Link] =
[Link]);
Q175 How do you schedule recurring jobs with [Link] and cron?
Answer
Create a class implementing IJob, build a JobDetail and a CronTrigger, schedule them via IScheduler.
Example
public class ReportJob : IJob {
public Task Execute(IJobExecutionContext ctx) =>
_report.GenerateAsync([Link]);
}
var job = [Link]<ReportJob>().Build();
var trigger = [Link]()
.WithCronSchedule("0 0 8 * * ?") // every day at 8 AM
.Build();
Answer
[Link](q => {
[Link]();
[Link]<ReportJob>(opts => [Link]("report"));
[Link](opts => opts
.ForJob("report")
.WithCronSchedule("0 0 8 * * ?"));
});
[Link](
q => [Link] = true);
Q177 How do you prevent the same job running on multiple instances simultaneously?
Answer
Four options: 1. [Link] clustered AdoJobStore (shared PostgreSQL) -- cluster elects one runner per job. 2.
Distributed lock (Redis Redlock or Azure Blob lease) -- first instance to acquire wins. 3. Hangfire with shared
database backend -- enqueuing is idempotent. 4. Outbox record with UNIQUE constraint -- only one instance can
insert the 'job started' record.
Example
Q178 How would you architect distributed scheduling across multiple servers?
Answer
Option A (clustered Quartz): shared AdoJobStore in PostgreSQL, cluster coordinates which node runs each job.
Automatic failover if leader goes down. Option B (message-driven): a single lightweight scheduler enqueues a
message (RabbitMQ, SQS). Worker pods consume and process. Scale workers independently; scheduler stays
simple.
Example
Ch 50 — Event Messaging
Answer
Answer
Use INotification instead of IRequest and call [Link](). Unlike Send, Publish calls ALL registered
handlers -- useful for domain events where multiple things need to react.
Example
public record OrderCreated(int OrderId) : INotification;
public class SendConfirmationEmail : INotificationHandler<OrderCreated> {
public Task Handle(OrderCreated n, CancellationToken ct)
=> _mailer.SendAsync([Link], ct);
}
await _mediator.Publish(new OrderCreated(orderId));
Answer
Every method call becomes invisible -- F12 takes you to MediatR's Send, not your handler. Pipeline behaviors add
indirection. For simple CRUD endpoints it is just extra ceremony. Use it where the decoupling genuinely adds
value -- complex CQRS flows, domain events.
Example
// Good fit: complex domain with many handlers and cross-cutting concerns
// Overkill: a simple CRUD action that just calls one service method
Answer
MassTransit is a .NET message bus abstraction. Write message contracts and consumer classes; MassTransit
handles transport (RabbitMQ, Azure Service Bus, SQS, Kafka), serialisation, consumer routing, retry policies,
sagas, and observability.
Example
[Link](x => {
[Link]<OrderCreatedConsumer>();
[Link]((ctx, cfg) => {
[Link]("rabbitmq://localhost");
[Link](ctx);
});
});
Answer
Call UsingRabbitMq inside AddMassTransit. ConfigureEndpoints reads your registered consumers and creates the
right queues and exchanges automatically.
Example
Answer
Declare message contracts as records. Implement IConsumer<TMessage>. MassTransit routes to the right
consumer based on the message type name.
Example
Answer
Sagas: state machines that persist state across multiple messages -- for long-running workflows. Retry policies:
exponential back-off on consumer exceptions, configurable per endpoint. Outbox: writes outgoing message inside
the same DB transaction as your domain changes -- guarantees exactly-once publishing.
Example
[Link]("order-placed", e => {
[Link](r => [Link](
5,
[Link](1),
[Link](5),
[Link](5)));
[Link]<OrderPlacedConsumer>(ctx);
});
Q186 What happens if you create a new HttpClient for every API call?
Answer
Each new HttpClient opens its own socket. Disposing it doesn't immediately close the socket -- it lingers in
TIME_WAIT for up to 4 minutes. Under load you quickly exhaust available ports (socket exhaustion). DNS changes
also go undetected because each client has its own DNS cache.
Example
Answer
HttpClientFactory manages a pool of HttpMessageHandler instances (default 2-minute lifetime). Handlers are
recycled to respect DNS changes while sockets are reused. Integrates cleanly with DI and Polly.
Example
[Link]("PaymentsApi", c =>
[Link] = new Uri("[Link]
Q188 What is a typed HttpClient and how does it differ from a named client?
Answer
Named client: inject IHttpClientFactory, call CreateClient("name") -- loose coupling but magic strings. Typed client:
inject a class that wraps HttpClient -- strongly typed, mockable in tests, no magic strings.
Example
Answer
Refit generates HttpClient implementations from annotated C# interfaces. You define the API contract with
attributes; Refit generates the HTTP calls, URL building, and serialisation.
Example
public interface IPaymentsApi {
[Post("/charges")]
Task<PaymentResult> ChargeAsync([Body] ChargeDto dto);
Answer
DelegatingHandler is middleware for the HttpClient pipeline. Override SendAsync, modify the request or response,
call the inner handler. Common uses: injecting auth tokens, logging, adding correlation IDs, implementing retry
logic.
Example
public class AuthHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage req, CancellationToken ct)
{
[Link] = new AuthenticationHeaderValue(
"Bearer", await GetTokenAsync());
return await [Link](req, ct);
}
}
Answer
Polly is a resilience library with policies for retry, circuit breaker, timeout, bulkhead, and fallback. Hook it into
HttpClientFactory with AddPolicyHandler -- the policy wraps every outgoing request from that client.
Example
[Link]<PaymentsClient>()
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.RetryAsync(3));
Q192 How do you add an exponential back-off retry policy with Polly?
Answer
Use HandleTransientHttpError (covers 5xx and network errors) and WaitAndRetryAsync with a back-off formula.
Example
[Link]<OrdersClient>()
.AddPolicyHandler(retry);
Answer
CircuitBreakerAsync opens the circuit after N consecutive failures, fast-failing all requests for a break duration.
After the break it lets one probe request through (half-open). Prevents hammering a failing downstream service.
var cb = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: [Link](30));
[Link]<OrdersClient>()
.AddPolicyHandler(cb);
Answer
FallbackAsync catches a failure and returns a pre-defined fallback response. Combine with circuit breaker and
retry for a full resilience stack.
Example
var fallback = Policy<HttpResponseMessage>
.HandleResult(r => ![Link])
.FallbackAsync(_ => {
var res = new HttpResponseMessage([Link]);
[Link] = new StringContent("{"source":"cache"}");
return [Link](res);
});
Q195 How do you handle token refresh for outgoing HTTP requests?
Answer
Use a DelegatingHandler that fetches a cached token and adds it as a Bearer header. On a 401 response,
invalidate the cache, get a fresh token, and retry once.
Example
public class BearerTokenHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage req, CancellationToken ct)
{
var token = await _cache.GetOrRefreshAsync(ct);
[Link] = new("Bearer", token);
var resp = await [Link](req, ct);
if ([Link] == [Link]) {
_cache.Invalidate();
token = await _cache.GetOrRefreshAsync(ct);
[Link] = new("Bearer", token);
resp = await [Link](req, ct);
}
return resp;
}
}
Answer
Traces -- distributed request flows across services, composed of spans linked by a trace ID. Metrics -- numeric
measurements over time: request rate, latency percentiles, error rate. Logs -- structured event records correlated
to traces via the trace ID.
Example
Answer
The Collector is a vendor-neutral agent that sits between your apps and your observability backends. Apps send
telemetry in standard OTLP format. The Collector filters, enriches, and fans out to Jaeger, Prometheus, Datadog,
etc. -- without your app code needing to know which backends you use.
Example
// [Link]:
// receivers: [otlp]
// exporters: [jaeger, prometheus]
// pipelines: traces->[jaeger], metrics->[prometheus]
Answer
A trace is the complete end-to-end journey of one request through all services, identified by a trace ID. A span is
one unit of work within that trace -- an HTTP call, a DB query. Spans have a start time, duration, parent span ID,
and arbitrary attributes, forming a tree.
Example
Answer
[Link] Core's OTel instrumentation automatically propagates W3C TraceContext headers (traceparent,
tracestate) on outgoing HttpClient calls and reads them from incoming requests. Baggage carries custom
key-value data along the same chain.
Example
[Link]("tenantId", tenantId);
// Included in all downstream HttpClient calls automatically
var tid = [Link]("tenantId");
Answer
AlwaysOn -- record everything. Only feasible for low-traffic. TraceIdRatioBased -- record a fixed percentage (e.g.,
10%). ParentBased -- inherit parent's sampling decision, keeping complete traces. Tail sampling -- decide after the
fact, keeping all error traces. Requires the Collector.
Example
[Link]()
.WithTracing(t => t
.SetSampler(new TraceIdRatioBasedSampler(0.1))
Answer
Use [Link] to create counters, histograms, and gauges. OTel picks them up via
AddMeter when you configure the metrics pipeline.
Example
Q202 What are trace links in OTel and when are they useful?
Answer
Trace links associate a span with spans from other traces. Classic use: a batch job links to each individual
request's trace it is processing, showing causal relationships without merging them into one giant trace.
Example
Answer
High cardinality (userId, request path with IDs as tags) creates a new time series per unique value -- can crash
your metrics backend. Use Views to allowlist only low-cardinality tags you actually need.
Example
[Link]()
.WithMetrics(m => m
.AddView("[Link]",
new MetricStreamConfiguration {
TagKeys = new[] { "region", "status" }
// userId and [Link] dropped
}));
Answer
Answer
Use -r with a Runtime Identifier (RID). Common: win-x64, linux-x64, linux-arm64, osx-arm64.
Example
# Single-file:
dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true --self-contained
Answer
[Link] # base
[Link] # prod overrides
[Link] # staging overrides
# Set at runtime:
# ASPNETCORE_ENVIRONMENT=Production
Answer
Use a multi-stage build: compile in the SDK image, copy only the published output into the lean runtime image.
Final image has no SDK, no source, no build tools.
Example
FROM [Link]/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app
FROM [Link]/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "[Link]"]
Answer
Multiple FROM instructions. Compile in a large SDK image, copy only the output into a lean runtime image. Final
image has no compiler, no source, no NuGet cache -- typically 3-5x smaller and much smaller attack surface.
Example
# Stage 1 -- build
FROM [Link]/dotnet/sdk:9.0 AS build
Q209 How do you minimise Docker image size for .NET apps?
Answer
Multi-stage build + runtime base image. Enable PublishTrimmed to remove unused assemblies. PublishSingleFile
packs everything into one binary. Native AOT produces the smallest result of all -- a standalone native binary.
Example
Answer
Use -e flags on docker run, the environment section in Docker Compose, or ENV in the Dockerfile for non-secret
defaults. For secrets use Docker Secrets or mount from a secrets manager -- never bake them into the image.
Example
docker run \
-e ASPNETCORE_ENVIRONMENT=Production \
-e ConnectionStrings__DefaultConnection="Host=db;" \
myapp
Q211 How do you write a Docker Compose file for [Link] Core + PostgreSQL + Redis?
Answer
Define three services. Use depends_on so the app doesn't start before the database. Pass connection strings as
environment variables.
Example
services:
app:
image: myapp
ports: ["8080:8080"]
environment:
- ConnectionStrings__Db=Host=postgres;Database=app;Username=app;Password=secret
- ConnectionStrings__Redis=redis:6379
depends_on: [postgres, redis]
postgres:
image: postgres:16
environment: { POSTGRES_PASSWORD: secret }
redis:
image: redis:7
Answer
Add a healthcheck block to the service. Docker polls it and marks the container healthy or unhealthy. Use
condition: service_healthy in depends_on to make the app wait until the DB is actually ready.
Example
Answer
.NET Aspire is an opinionated stack for cloud-native apps. Instead of writing Docker Compose or Helm charts, you
declare your services, databases, caches, and message buses in a C# AppHost project. Aspire starts all
containers locally, provides a built-in dashboard with distributed traces and metrics, and handles service discovery
wiring.
Example
var builder = [Link](args);
var db = [Link]("db");
var cache = [Link]("cache");
[Link]<[Link]>("orders-api")
.WithReference(db)
.WithReference(cache);
[Link]().Run();
Answer
Run the Aspire publish command to generate a [Link] from the AppHost definition, then deploy
normally.
Example
Answer