πŸ”· C# 13 .NET 9 ASP.NET Core EF Core ~100 questions

Senior C# / .NET Developer

A complete set of senior-level C# and .NET interview questions covering language internals, the CLR, memory management, async/await, LINQ, ASP.NET Core, Entity Framework Core, design patterns, testing, performance, microservices, and AI/ML integration.

No questions match your search. Try a different keyword.

C# Core Language

10 questions
1What is the difference between value types and reference types in C#? Explain boxing and unboxing.

Value types (structs, enums, primitives like int, bool, double) store their data directly on the stack (or inline in a containing structure). Assignment copies the value. Equality is by value by default.

Reference types (classes, interfaces, delegates, arrays, strings) store a reference (pointer) on the stack pointing to data allocated on the managed heap. Assignment copies the reference β€” two variables can point to the same object.

Boxing β€” implicitly converting a value type to object or an interface type. The runtime allocates a heap object, copies the value into it, and returns a reference. This is an allocation β€” it generates garbage.

Unboxing β€” explicitly converting the boxed object back to a value type. Requires a cast and throws InvalidCastException if the types don't match.

int value = 42;
object boxed = value;        // boxing β€” heap allocation, GC pressure
int unboxed = (int)boxed;    // unboxing β€” must match original type
double bad = (double)boxed;  // InvalidCastException at runtime!

// Common boxing pitfalls:
// 1. Non-generic collections (ArrayList, Hashtable) β€” box every int/bool
ArrayList list = new ArrayList();
list.Add(42);   // boxes 42 β†’ avoid, use List<int>

// 2. Interface on a struct
interface IFoo { void Do(); }
struct MyStruct : IFoo { public void Do() { } }
IFoo foo = new MyStruct(); // boxing β€” stored as object on heap

// 3. String.Format / string concatenation with value types
string s = "Value: " + 42; // 42 gets boxed in some older patterns
// Use string interpolation or spans to avoid

// Detect boxing in hot paths via BenchmarkDotNet or PerfView

Boxing is a significant source of hidden allocations in performance-sensitive code. Use generics (List<T>, Dictionary<K,V>), Span<T>, and readonly struct to avoid it.

2Explain the difference between struct and class in C#. When should you use a struct?

Structs are value types; classes are reference types. Key differences:

  • Storage β€” struct: stack or inline in containing type. Class: heap allocation with GC overhead.
  • Assignment β€” struct: copies all fields. Class: copies the reference.
  • Inheritance β€” structs cannot inherit from other structs or classes (except implicitly from ValueType). They can implement interfaces.
  • Default value β€” struct instances are always valid (zeroed memory); class references can be null.
  • Nullable β€” struct? (Nullable<T>) wraps a struct to allow null.
// Use struct when all of these apply:
// 1. Logically represents a single value (like a number)
// 2. Small β€” typically ≀16 bytes (larger structs copy overhead exceeds heap benefit)
// 3. Immutable (or should behave immutably)
// 4. Will not be boxed frequently

readonly struct Point3D(double X, double Y, double Z); // C# 12 primary constructor

// ref struct β€” stack-only; cannot be boxed, stored in heap, or used in async
ref struct StackBuffer {
  public Span<byte> Data;
}

// Record structs (C# 10) β€” value semantics + built-in equality + with expression
record struct Color(byte R, byte G, byte B);
var red = new Color(255, 0, 0);
var lighter = red with { R = 200 }; // non-destructive mutation

When NOT to use struct: when the type needs to be nullable in an idiomatic way, when it's large (>16 bytes and frequently passed by value), when it needs to inherit from another type, or when it will be stored in non-generic collections (boxing).

3What are delegates, events, and lambda expressions? How do they relate to each other?

A delegate is a type-safe function pointer β€” a reference type that holds a reference to a method (or list of methods for multicast delegates). Func<T, TResult> and Action<T> are the built-in generic delegate types.

Lambda expressions are anonymous functions that can be assigned to delegate types or expression trees. They capture variables from their enclosing scope (closures).

Events are a publisher-subscriber mechanism built on delegates. The event keyword restricts access β€” external code can only subscribe/unsubscribe (+=/-=), not invoke or replace the delegate directly.

// Delegate declaration and usage
delegate int MathOp(int a, int b);
MathOp add = (a, b) => a + b;       // lambda assigned to delegate
MathOp multiply = (a, b) => a * b;
MathOp combo = add + multiply;      // multicast β€” both invoked
Console.WriteLine(combo(3, 4));     // prints 7 then 12, returns last result (12)

// Built-in delegate types (prefer these over custom delegates)
Func<int, int, int> addFunc = (a, b) => a + b;     // has return value
Action<string> print = msg => Console.WriteLine(msg); // void return
Predicate<int> isEven = n => n % 2 == 0;              // bool return

// Events β€” proper encapsulation
class Button {
  public event EventHandler<ClickEventArgs>? Clicked;

  protected virtual void OnClicked(ClickEventArgs e)
    => Clicked?.Invoke(this, e); // thread-safe null check with ?.

  public void SimulateClick() => OnClicked(new ClickEventArgs());
}

var btn = new Button();
btn.Clicked += (sender, e) => Console.WriteLine("Clicked!");
// btn.Clicked = null;  // ❌ compile error β€” events protect the delegate
4What are generics in C#? Explain constraints, covariance, and contravariance.

Generics allow writing type-safe, reusable code without boxing. Unlike Java's type erasure, C# generics are reified β€” the CLR creates specialised JIT-compiled code for each value type argument at runtime.

// Generic method with constraints
public T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) >= 0 ? a : b;

// Multiple constraints
public class Repository<T> where T : class, IEntity, new()
{
    public T Create() => new T(); // new() constraint enables this
}

// Constraint types:
// where T : class          β€” reference type
// where T : struct         β€” value type (non-nullable)
// where T : new()          β€” public parameterless constructor
// where T : SomeBaseClass  β€” must inherit from SomeBaseClass
// where T : ISomeInterface β€” must implement interface
// where T : notnull        β€” non-nullable (C# 8+)
// where T : unmanaged      β€” unmanaged type (C# 7.3+)

Covariance (out T) β€” a generic type can be used as its base type. IEnumerable<Dog> can be used as IEnumerable<Animal> because Dog extends Animal. Safe for read-only (producer) interfaces.

Contravariance (in T) β€” a generic type can be used as its derived type. Action<Animal> can be used as Action<Dog> because a method that handles any animal can certainly handle a dog. Safe for write-only (consumer) interfaces.

// Covariant β€” IEnumerable<out T>
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs;  // βœ… covariant β€” Dog IS-A Animal

// Contravariant β€” IComparer<in T>
IComparer<Animal> animalComparer = Comparer<Animal>.Default;
IComparer<Dog> dogComparer = animalComparer; // βœ… contravariant

// Custom covariant interface
interface IProducer<out T> { T Produce(); }
// Custom contravariant interface
interface IConsumer<in T> { void Consume(T item); }
5What are expression trees in C#? How does LINQ to SQL/EF use them?

An expression tree represents code as a data structure (an abstract syntax tree) rather than as executable bytecode. It is a tree of Expression objects that can be inspected, modified, and compiled at runtime.

When you assign a lambda to Expression<Func<T, bool>> instead of Func<T, bool>, the C# compiler generates expression tree construction code rather than IL bytecode for the lambda body.

// Func β€” compiled delegate (runs in .NET)
Func<int, bool> isEven = n => n % 2 == 0;
isEven(4); // executes the IL directly

// Expression tree β€” data structure (can be inspected and translated)
Expression<Func<int, bool>> isEvenExpr = n => n % 2 == 0;
// isEvenExpr is an object graph:
// BinaryExpression (==)
//   β”œβ”€β”€ BinaryExpression (%)
//   β”‚   β”œβ”€β”€ ParameterExpression (n)
//   β”‚   └── ConstantExpression (2)
//   └── ConstantExpression (0)

// EF Core translates LINQ expression trees to SQL:
Expression<Func<User, bool>> filter = u => u.Age > 18 && u.IsActive;
var users = dbContext.Users.Where(filter).ToList();
// EF Core walks the expression tree and generates:
// WHERE Age > 18 AND IsActive = 1

// Manual compilation β€” run the expression tree as a delegate
var compiled = isEvenExpr.Compile();
compiled(4); // executes the compiled delegate

// Dynamic query building β€” build expressions programmatically
var param = Expression.Parameter(typeof(User), "u");
var prop  = Expression.Property(param, "Age");
var val   = Expression.Constant(18);
var body  = Expression.GreaterThan(prop, val);
var lambda = Expression.Lambda<Func<User, bool>>(body, param);
// Equivalent to: u => u.Age > 18
6What are nullable reference types (NRTs) in C# 8+? How do they improve null safety?

Before C# 8, all reference types were implicitly nullable β€” any reference could be null without compiler warning, leading to runtime NullReferenceExceptions. Nullable reference types (NRTs) add compile-time null analysis.

When enabled (<Nullable>enable</Nullable> in csproj), the compiler tracks nullability flow and warns when you dereference a potentially null reference or assign null to a non-nullable reference.

// NRT enabled:
string name = "Alice";      // non-nullable β€” compiler assumes never null
string? nullableName = null; // explicitly nullable

void Print(string name) {
    Console.WriteLine(name.Length); // safe β€” name is non-nullable
}

void PrintMaybe(string? name) {
    Console.WriteLine(name.Length); // ⚠️ warning: 'name' may be null
    Console.WriteLine(name?.Length ?? 0); // safe β€” null-conditional
    if (name is not null) Console.WriteLine(name.Length); // safe β€” narrowed
}

// Null-forgiving operator β€” suppress warning when you know better
string forceNonNull = GetMaybeNull()!; // ! says "trust me, not null"

// Attributes for library authors
class Parser {
    [return: MaybeNull]   // may return null even if T is non-nullable
    public T? Parse<T>(string input) { ... }

    [NotNullWhen(true)]   // out param is non-null when method returns true
    public bool TryParse(string input, [NotNullWhen(true)] out string? result) { ... }
}

Enable NRTs on all new projects. For existing codebases, migrate incrementally using #nullable enable/#nullable disable per file. NRTs are a compile-time feature only β€” no runtime behaviour change, but catches a huge class of bugs.

7What are records in C#? How do they differ from classes and structs? When do you use them?

Records (C# 9+) are reference types with value-based equality semantics and built-in immutability support. The compiler auto-generates: constructor, Deconstruct, ToString, Equals, GetHashCode, and the with expression support.

// Positional record β€” concise syntax
record Person(string FirstName, string LastName, int Age);

var alice = new Person("Alice", "Smith", 30);
var bob   = alice with { FirstName = "Bob", Age = 25 }; // non-destructive mutation

// Value-based equality
var alice2 = new Person("Alice", "Smith", 30);
alice == alice2;  // true β€” compares all properties (unlike class: reference equality)
alice.Equals(alice2); // true

// Record with additional members
record Product(string Name, decimal Price) {
    public decimal PriceWithTax => Price * 1.2m;

    // Validate in constructor
    public Product {
        if (Price < 0) throw new ArgumentException("Price cannot be negative");
    }
}

// record struct (C# 10) β€” value type with record features
record struct Point(double X, double Y);

// record class (explicit β€” same as record, reference type)
record class Order(Guid Id, List<OrderItem> Items);

Use records for: DTOs, Value Objects (DDD), command/query objects, API request/response models, immutable data carriers. Don't use for: entities with mutable identity (use classes), types with complex constructor logic that can't be expressed as init-only properties.

8What is pattern matching in C#? Cover switch expressions, property patterns, and list patterns.

Pattern matching in C# allows concise conditional logic based on type, shape, and value of data. It has evolved significantly from C# 7 through C# 11.

// Type pattern + switch expression (C# 8)
string Describe(object obj) => obj switch {
    int n when n < 0 => "negative integer",
    int n            => $"positive integer: {n}",
    string s         => $"string of length {s.Length}",
    null             => "null",
    _                => "something else",
};

// Property pattern (C# 8)
bool IsAdultInUS(Person p) => p is { Age: >= 18, Country: "US" };

decimal GetShipping(Order order) => order switch {
    { Weight: > 50, IsPriority: true }  => 49.99m,
    { Weight: > 50 }                    => 19.99m,
    { IsPriority: true }                => 9.99m,
    _                                   => 4.99m,
};

// Positional pattern β€” deconstructs the object
decimal Area(Shape shape) => shape switch {
    Circle(var r)         => Math.PI * r * r,
    Rectangle(var w, var h) => w * h,
    Triangle(var b, var h)  => 0.5 * b * h,
    _ => throw new InvalidOperationException(),
};

// List patterns (C# 11)
string Classify(int[] arr) => arr switch {
    []            => "empty",
    [var x]       => $"single: {x}",
    [var x, var y] => $"pair: {x}, {y}",
    [> 0, ..]     => "starts positive",
    _             => "other",
};
9What are iterators and the yield keyword in C#? How do they enable lazy evaluation?

The yield keyword transforms a method into a state machine that implements IEnumerable<T> or IEnumerator<T>. The method body pauses at each yield return and resumes on the next call to MoveNext(). Values are produced lazily β€” only when the consumer requests them.

// Iterator method β€” produces values one at a time
IEnumerable<int> Fibonacci() {
    int a = 0, b = 1;
    while (true) {
        yield return a;         // pause, return current value
        (a, b) = (b, a + b);    // resumes here on next iteration
    }
}

// Lazy β€” only evaluates as needed
foreach (var n in Fibonacci().Take(10))
    Console.Write(n + " "); // 0 1 1 2 3 5 8 13 21 34

// Infinite sequence β€” no stack overflow (unlike naive recursion)
// because state machine persists between calls

// yield break β€” end the sequence early
IEnumerable<T> TakeWhilePositive<T>(IEnumerable<T> source)
    where T : IComparable<T> {
    foreach (var item in source) {
        if (item.CompareTo(default!) <= 0) yield break;
        yield return item;
    }
}

// C# compiler generates a state machine class:
// - Stores local variables as fields
// - Tracks execution state via a state integer
// - Implements IEnumerable/IEnumerator

// Async iterators (C# 8) β€” IAsyncEnumerable<T>
async IAsyncEnumerable<int> GetItemsAsync(
    [EnumeratorCancellation] CancellationToken ct = default) {
    for (int i = 0; i < 100; i++) {
        await Task.Delay(10, ct);    // async operation per item
        yield return i;
    }
}

await foreach (var item in GetItemsAsync())
    Console.WriteLine(item);
10What are the new C# 12 and C# 13 features? What major improvements do they bring?

C# 12 (.NET 8):

// Primary constructors on any class/struct (not just records)
class HttpClient(string baseUrl, ILogger logger) {
    public async Task<T> GetAsync<T>(string path) {
        logger.LogInformation("GET {url}", baseUrl + path);
        // ...
    }
}

// Collection expressions β€” unified syntax for collections
int[] arr       = [1, 2, 3];
List<int> list  = [1, 2, 3];
Span<int> span  = [1, 2, 3];
int[] combined  = [..arr, 4, 5, ..list]; // spread operator

// Inline arrays β€” fixed-size arrays on the stack (for performance)
[InlineArray(10)]
struct Buffer { private int _element; }  // 10 ints on stack

// Default lambda parameters
var fn = (int x, int multiplier = 2) => x * multiplier;
fn(5);     // 10
fn(5, 3);  // 15

// Alias any type (not just named types)
using Matrix = int[,];
using Handler = Func<HttpContext, Task>;

C# 13 (.NET 9):

// params with any collection type (not just arrays)
void Print(params IEnumerable<string> items) { }
void Print(params ReadOnlySpan<int> values) { }

// New lock type β€” System.Threading.Lock (better than object lock)
private readonly Lock _lock = new();
lock (_lock) { /* critical section */ }

// Partial properties and indexers
partial class MyClass {
    public partial string Name { get; set; }
}

// ref and unsafe in iterators and async methods (selected scenarios)

// Overload resolution priority attribute
[OverloadResolutionPriority(1)]
public void Process(ReadOnlySpan<byte> data) { } // preferred
public void Process(byte[] data) { }               // fallback

CLR & Memory

8 questions
1How does the .NET garbage collector work? Explain generations, LOH, and GC modes.

The .NET GC is a generational, tracing garbage collector. It uses the observation that most objects are short-lived.

Generations:

  • Gen 0 β€” newly allocated objects (~256 KB budget). Collected most frequently (hundreds of times per second on a busy app). Very fast β€” usually 1ms or less.
  • Gen 1 β€” objects that survived one Gen 0 collection (~2 MB budget). Buffer between short and long-lived objects.
  • Gen 2 β€” long-lived objects (static data, caches, large data structures). Collected infrequently β€” full GC (stop-the-world pause). Compacts the heap to reduce fragmentation.

Large Object Heap (LOH) β€” objects β‰₯85,000 bytes (roughly) go directly here. LOH is Gen 2 and historically was never compacted (fragmentation issue). Since .NET 4.5.1, LOH compaction can be triggered manually: GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce.

GC Modes:

  • Workstation GC β€” single dedicated GC thread, shorter pauses, lower throughput. Default for desktop apps.
  • Server GC β€” one GC heap and thread per logical CPU. Higher throughput, potentially longer pauses. Default for ASP.NET Core. Enable with <GarbageCollectionAdapationMode> or in csproj.
  • Background GC β€” Gen 2 collections happen concurrently with application threads. Reduces pause times significantly.
// Trigger a GC (avoid in production β€” GC knows best)
GC.Collect(2, GCCollectionMode.Forced, blocking: true);

// Check memory pressure
long totalMemory = GC.GetTotalMemory(forceFullCollection: false);
GCMemoryInfo info = GC.GetGCMemoryInfo();
Console.WriteLine($"Heap size: {info.HeapSizeBytes / 1024 / 1024} MB");

// Register for GC notifications (Server GC)
GC.RegisterForFullGCNotification(10, 10);
2What is IDisposable and the Dispose pattern? What is the difference between Dispose and finalizers?

The IDisposable pattern enables deterministic release of unmanaged resources (file handles, database connections, network sockets, native memory) that the GC doesn't manage.

// Full Dispose pattern β€” when you hold unmanaged resources directly
public class ResourceHolder : IDisposable {
    private bool _disposed = false;
    private IntPtr _unmanagedHandle;
    private ManagedResource? _managed;

    ~ResourceHolder() {        // finalizer β€” GC calls this if Dispose wasn't called
        Dispose(false);        // only clean unmanaged resources
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this); // prevent finalizer from running β€” already cleaned up
    }

    protected virtual void Dispose(bool disposing) {
        if (_disposed) return;
        if (disposing) {
            _managed?.Dispose(); // dispose managed resources only when called from Dispose()
        }
        ReleaseUnmanagedHandle(_unmanagedHandle); // always release unmanaged
        _disposed = true;
    }
}

// Simple pattern β€” when you only hold other IDisposable objects
public class DbService(IDbConnection connection) : IDisposable {
    public void Dispose() => connection.Dispose();
}

// Always use using β€” guaranteed Dispose even on exception
using var conn = new SqlConnection(connStr);
await using var stream = new FileStream(path, FileMode.Open); // async dispose

Finalizer vs Dispose: Finalizers are called by the GC on an unpredictable thread at an unpredictable time (could be seconds or minutes later). They add objects to the finalization queue, causing them to survive at least one extra GC cycle. Dispose is deterministic β€” called immediately when using exits. Always prefer Dispose; use finalizers only as a safety net for unmanaged resources.

3What are Span<T>, Memory<T>, and ArrayPool<T>? How do they reduce allocations?

Span<T> β€” a ref struct that represents a contiguous region of memory. Stack-only β€” cannot be stored on the heap, in async methods, or in closures. Zero-copy slicing of arrays, strings, and stack-allocated memory.

Memory<T> β€” heap-safe wrapper around a contiguous memory region. Can be stored as a field, used in async methods, and passed to lambdas. Provides a Span<T> view via .Span.

ArrayPool<T> β€” rents arrays from a pool instead of allocating, avoiding GC pressure for temporary large arrays.

// Span β€” zero-copy string parsing (no substring allocations)
string csv = "Alice,30,Engineer";
ReadOnlySpan<char> span = csv.AsSpan();

int first = span.IndexOf(',');
ReadOnlySpan<char> name = span[..first];   // "Alice" β€” no heap allocation
span = span[(first + 1)..];
int second = span.IndexOf(',');
ReadOnlySpan<char> ageSpan = span[..second]; // "30" β€” no allocation
int age = int.Parse(ageSpan);               // parses without converting to string

// Stack allocation β€” for small, short-lived buffers
Span<byte> buffer = stackalloc byte[256];   // on stack, no GC
ReadOnlySpan<byte> data = GetData();
data.CopyTo(buffer);

// ArrayPool β€” rent/return large arrays
byte[] rented = ArrayPool<byte>.Shared.Rent(4096); // may be larger than requested
try {
    int read = stream.Read(rented, 0, 4096);
    Process(rented.AsSpan(0, read));
} finally {
    ArrayPool<byte>.Shared.Return(rented, clearArray: false);
}

// Memory β€” for async scenarios
async Task ProcessAsync(Memory<byte> buffer) {
    int read = await stream.ReadAsync(buffer);
    // process buffer.Span[..read]
}
4What are weak references in .NET? When do you use them to avoid memory leaks?

A weak reference (WeakReference<T>) allows you to reference an object without preventing it from being garbage collected. The GC can collect the referenced object even if the weak reference still exists; attempting to access it after collection returns false from TryGetTarget.

// Caching without memory leak β€” GC can collect values under pressure
class WeakCache<TKey, TValue> where TValue : class {
    private readonly Dictionary<TKey, WeakReference<TValue>> _cache = new();

    public TValue? Get(TKey key) {
        if (_cache.TryGetValue(key, out var weakRef)
            && weakRef.TryGetTarget(out var value))
            return value;
        return null;
    }

    public void Set(TKey key, TValue value) {
        _cache[key] = new WeakReference<TValue>(value);
    }
}

// Common memory leak pattern β€” event subscriptions
class Publisher {
    public event EventHandler? Published;
}
class Subscriber {
    public Subscriber(Publisher pub) {
        pub.Published += OnPublished; // strong reference β€” Subscriber won't be GC'd
    }
    void OnPublished(object? s, EventArgs e) { }
}
// Fix: unsubscribe in Dispose, or use WeakEventManager

// ConditionalWeakTable β€” attach data to an object without rooting it
var table = new ConditionalWeakTable<object, string>();
table.Add(someObj, "metadata"); // entry removed when someObj is GC'd
5What is unsafe code and pointers in C#? When is it appropriate to use them?

Unsafe code in C# (blocks/methods marked unsafe) allows pointer arithmetic, direct memory manipulation, and interop with native code β€” bypassing the CLR's type safety guarantees. Requires <AllowUnsafeBlocks>true</AllowUnsafeBlocks> in csproj.

// Pointer operations
unsafe void ProcessBuffer(byte* buffer, int length) {
    for (int i = 0; i < length; i++) {
        *(buffer + i) = 0; // zero out each byte
    }
}

// fixed β€” pin managed objects so GC doesn't move them during pointer use
unsafe void CopyData(byte[] source, byte[] destination) {
    fixed (byte* src = source, dst = destination) {
        Buffer.MemoryCopy(src, dst, destination.Length, source.Length);
    }
}

// stackalloc β€” allocate on the stack (avoid heap allocation for small buffers)
unsafe void HashData(ReadOnlySpan<byte> data) {
    byte* buffer = stackalloc byte[32]; // 32 bytes on stack β€” no GC
    // use buffer...
}
// Prefer Span version (no unsafe keyword required):
Span<byte> buffer = stackalloc byte[32]; // same memory, but type-safe

When to use unsafe: high-performance scenarios where every allocation matters (image processing, cryptography, audio codecs), P/Invoke interop with native libraries, implementing low-level data structures. In most cases, Span<T>, Memory<T>, and NativeMemory provide performance without unsafe code.

6How does the CLR handle type safety and the Common Type System (CTS)?

The Common Language Runtime (CLR) is the execution engine for .NET. It provides: JIT compilation (IL β†’ native machine code), memory management (GC), exception handling, security, and type safety enforcement.

The Common Type System (CTS) defines how types are declared, used, and managed in the CLR. All .NET languages (C#, F#, VB.NET) compile to the same Intermediate Language (IL) and share the CTS β€” enabling cross-language interoperability.

The Common Language Specification (CLS) is a subset of CTS that all .NET languages must support to ensure interoperability. CLS-compliant types can be used from any .NET language.

// IL (Intermediate Language) β€” what C# compiles to
// All .NET code compiles to platform-independent IL
// The CLR's JIT compiler turns IL into native code at runtime

// JIT modes:
// - Default JIT: compile methods on first call
// - AOT (Ahead-of-Time): NativeAOT compiles entire app at build time
//   β†’ fast startup, smaller footprint (no JIT overhead), important for serverless

// Verify CLR version
Console.WriteLine(RuntimeInformation.FrameworkDescription);
// ".NET 9.0.x"

// Tiered compilation (default in .NET Core):
// Tier 0: fast compile, no optimisation (first invocation)
// Tier 1: full JIT optimisation (after method called N times)
// This balances startup time vs peak throughput
7What is NativeAOT and how does it change .NET deployment? What are its limitations?

NativeAOT (Ahead-of-Time compilation, stable in .NET 7+) compiles .NET applications directly to native machine code at build time, eliminating the need for the JIT at runtime and drastically reducing startup time and memory footprint.

Benefits:

  • Near-instant startup β€” no JIT warm-up (sub-millisecond for simple apps vs 100ms+ for JIT).
  • Lower memory β€” no JIT compiler, no IL metadata in memory.
  • Smaller deployment β€” tree shaking removes unused code. Single native executable.
  • Ideal for: serverless functions (cold starts matter), CLI tools, microservices, containers, embedded systems.

Limitations:

  • No runtime code generation β€” Reflection.Emit, dynamic proxies, and some dynamic patterns don't work without modifications.
  • Reflection is limited β€” only types/members accessed at compile-time (or explicitly annotated with [DynamicallyAccessedMembers]) are preserved.
  • Source generators must be used instead of runtime reflection for libraries (e.g., System.Text.Json source generation).
  • Platform-specific builds β€” must compile for each target OS/arch.
// csproj β€” enable NativeAOT
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

// Publish
// dotnet publish -r linux-x64 -c Release

// Trim-compatible code β€” avoid dynamic patterns
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
Type type = typeof(MyModel); // tells linker to keep public properties
8What are source generators in C#? How do they replace runtime reflection for performance?

Source generators are Roslyn extensions that run during compilation and generate additional C# source code. They receive the current compilation as input (syntax trees, semantic model) and emit new source files that are compiled alongside user code.

They replace runtime reflection patterns with compile-time code generation β€” zero runtime cost, AOT-compatible, and with full IDE support (auto-complete, refactoring).

// System.Text.Json source generation β€” avoid runtime reflection for serialisation
[JsonSerializable(typeof(WeatherForecast))]
[JsonSerializable(typeof(List<WeatherForecast>))]
internal partial class AppJsonContext : JsonSerializerContext { }

// Usage β€” uses generated code, not reflection
var json = JsonSerializer.Serialize(forecast, AppJsonContext.Default.WeatherForecast);

// Logging source generator β€” structured logging without boxing
public partial class MyService(ILogger<MyService> logger) {
    [LoggerMessage(Level = LogLevel.Information, Message = "Processing order {OrderId}")]
    private partial void LogProcessing(int orderId); // compiler generates the method body

    public void Process(int orderId) {
        LogProcessing(orderId); // zero-allocation, no boxing of orderId
    }
}

// Other popular source generators:
// - AutoMapper.Extensions.Microsoft.DependencyInjection
// - Refit (HTTP client generation from interfaces)
// - Mapperly (object mapping)
// - Microsoft.Extensions.Options (configuration binding)
// - System.Runtime.InteropServices.LibraryImportAttribute (P/Invoke)

Async & Concurrency

9 questions
1How does async/await work in C#? What does the compiler generate?

The C# compiler transforms async methods into a state machine class. Each await point becomes a state. When the awaited task completes, the continuation (code after await) is scheduled β€” typically on the original synchronisation context (ASP.NET Core has none by default, so continuations run on thread pool threads).

// What you write:
async Task<string> GetDataAsync(string url) {
    var client = new HttpClient();
    var response = await client.GetAsync(url);         // await point 1
    var content  = await response.Content.ReadAsStringAsync(); // await point 2
    return content.ToUpper();
}

// What the compiler generates (simplified):
class GetDataAsyncStateMachine : IAsyncStateMachine {
    int _state = -1;
    AsyncTaskMethodBuilder<string> _builder;
    HttpClient _client;
    HttpResponseMessage _response;
    string _content;
    TaskAwaiter<HttpResponseMessage> _awaiter1;
    TaskAwaiter<string> _awaiter2;

    void MoveNext() {
        switch (_state) {
            case -1: // initial
                _client = new HttpClient();
                _awaiter1 = _client.GetAsync(url).GetAwaiter();
                if (!_awaiter1.IsCompleted) {
                    _state = 0;
                    _builder.AwaitUnsafeOnCompleted(ref _awaiter1, ref this);
                    return; // yield β€” return control to caller
                }
                goto case 0;
            case 0: // GetAsync completed
                _response = _awaiter1.GetResult();
                _awaiter2 = _response.Content.ReadAsStringAsync().GetAwaiter();
                if (!_awaiter2.IsCompleted) { _state = 1; /* yield */ return; }
                goto case 1;
            case 1: // ReadAsStringAsync completed
                _content = _awaiter2.GetResult();
                _builder.SetResult(_content.ToUpper());
                break;
        }
    }
}

Key insight: async/await does NOT create threads. It is cooperative concurrency β€” the current thread is released at each await point and reused for other work. Threads come from the thread pool when continuations are scheduled.

2What is ConfigureAwait(false) and when should you use it?

By default, await captures the current SynchronizationContext (if any) and resumes execution on it after the awaited task completes. In a Windows Forms or WPF app, this means continuations run on the UI thread. In classic ASP.NET (not Core), there's a request-specific context.

ConfigureAwait(false) tells the await not to capture the synchronisation context β€” the continuation runs on whatever thread pool thread is available. This is faster (no context switching) and avoids deadlocks in certain scenarios.

// Library code β€” always use ConfigureAwait(false)
// (Library doesn't know if caller has a SynchronizationContext)
public async Task<string> FetchDataAsync(string url) {
    var response = await httpClient.GetAsync(url).ConfigureAwait(false);
    var content  = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    return content;
}

// Classic deadlock scenario (ASP.NET non-Core / WPF):
// 1. UI thread calls GetAwaiter().GetResult() β€” blocks the UI thread
// 2. async method tries to resume on UI thread (captured context)
// 3. UI thread is blocked β€” DEADLOCK
public string GetDataSync() {
    return FetchDataAsync("http://api.example.com").Result; // deadlock risk!
}
// ConfigureAwait(false) in FetchDataAsync fixes this

// ASP.NET Core β€” no SynchronizationContext by default
// ConfigureAwait(false) still recommended for performance (avoids null check)
// and for library code that may be used in non-Core contexts

Rule: in library/infrastructure code, always use ConfigureAwait(false). In application code (ASP.NET Core controllers, Blazor), it's less critical but still a good habit. Never call .Result or .Wait() on async methods in async code β€” use await.

3What is ValueTask<T> and when should you use it instead of Task<T>?

Task<T> is a heap-allocated reference type. Even if the result is immediately available (cache hit, synchronous completion), creating a Task allocates an object on the heap. For hot paths called millions of times, this allocation adds significant GC pressure.

ValueTask<T> is a struct that can hold either a result directly (if synchronously available) or a Task<T> (if actual async work was needed). When the operation completes synchronously, there's zero allocation.

// Cache example β€” frequently returns synchronously
private readonly Dictionary<int, User> _cache = new();

// With Task β€” allocates a Task even for cache hits
public Task<User?> GetUserAsync(int id) {
    if (_cache.TryGetValue(id, out var user))
        return Task.FromResult(user);         // allocates a Task
    return LoadFromDbAsync(id);
}

// With ValueTask β€” zero allocation on cache hits
public ValueTask<User?> GetUserAsync(int id) {
    if (_cache.TryGetValue(id, out var user))
        return new ValueTask<User?>(user);   // no allocation β€” result in struct
    return new ValueTask<User?>(LoadFromDbAsync(id)); // wraps Task when needed
}

// IMPORTANT restrictions on ValueTask:
// 1. Only await once β€” ValueTask is not safe to await multiple times
// 2. Don't store and await later β€” consume immediately
// 3. Don't call .Result multiple times

// When to use ValueTask:
// βœ… Hot path where synchronous completion is the common case
// βœ… Interface methods that implementors might complete synchronously
// βœ… IAsyncEnumerable<T> (always use ValueTask internally)
// ❌ Fire-and-forget, background tasks, or when always truly async
4What is CancellationToken and how do you implement cooperative cancellation?

CancellationToken enables cooperative cancellation β€” the code performing the work must periodically check the token and stop cleanly. Cancellation is requested via a CancellationTokenSource.

// Creating and using a CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // auto-cancel after 30s
CancellationToken token = cts.Token;

// Cancel manually
cts.Cancel();
// Cancel after timeout (alternative)
cts.CancelAfter(TimeSpan.FromSeconds(5));

// Implementing cancellable async work
async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken ct) {
    foreach (var item in items) {
        ct.ThrowIfCancellationRequested(); // check before each item
        await ProcessAsync(item, ct);      // pass token to all async calls
    }
}

// Checking without throwing (for cleanup scenarios)
while (!ct.IsCancellationRequested) {
    await DoWorkAsync(ct);
}

// Link tokens β€” cancel if either source cancels
using var linked = CancellationTokenSource.CreateLinkedTokenSource(userCt, timeoutCt);

// ASP.NET Core β€” request cancellation automatically
[HttpGet("slow")]
public async Task<IActionResult> SlowAction(CancellationToken ct) {
    // ct is automatically cancelled when the HTTP request is aborted
    var data = await _service.GetDataAsync(ct);
    return Ok(data);
}

// Register cleanup on cancellation
ct.Register(() => Console.WriteLine("Cancellation requested β€” cleaning up"));
5What are SemaphoreSlim, Channel<T>, and Mutex? When do you use each?
  • SemaphoreSlim β€” limits concurrent access to a resource. Supports async wait (WaitAsync). Use for throttling concurrency (e.g., max 5 concurrent HTTP requests, max N concurrent DB connections).
  • Channel<T> β€” a thread-safe, async-first producer-consumer queue. Replaces BlockingCollection<T> for async scenarios. Bounded or unbounded. Use for pipeline patterns and work queuing.
  • Mutex β€” a system-wide (cross-process) mutual exclusion lock. Heavier than lock or SemaphoreSlim. Use only for cross-process synchronisation (named mutex) β€” prefer lock/SemaphoreSlim for in-process.
// SemaphoreSlim β€” limit concurrent API calls
private readonly SemaphoreSlim _throttle = new(maxCount: 5, initialCount: 5);

async Task<string[]> FetchAllAsync(IEnumerable<string> urls) {
    var tasks = urls.Select(async url => {
        await _throttle.WaitAsync();
        try   { return await httpClient.GetStringAsync(url); }
        finally { _throttle.Release(); }
    });
    return await Task.WhenAll(tasks);
}

// Channel β€” producer-consumer pipeline
var channel = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(100) {
    FullMode = BoundedChannelFullMode.Wait, // backpressure
});

// Producer
async Task ProduceAsync(ChannelWriter<WorkItem> writer, CancellationToken ct) {
    await foreach (var item in GetItemsAsync(ct))
        await writer.WriteAsync(item, ct);
    writer.Complete();
}

// Consumer
async Task ConsumeAsync(ChannelReader<WorkItem> reader, CancellationToken ct) {
    await foreach (var item in reader.ReadAllAsync(ct))
        await ProcessAsync(item);
}
6What is the difference between Task.Run, Task.Factory.StartNew, and Thread? When do you create threads explicitly?
  • Task.Run(action) β€” queues work to the thread pool. Uses TaskCreationOptions.DenyChildAttach (safe default). Preferred for CPU-bound work offloaded from the calling thread. Returns a Task you can await.
  • Task.Factory.StartNew(action, options) β€” more control (custom scheduler, LongRunning, AttachedToParent). LongRunning hint asks the scheduler to create a dedicated thread (doesn't starve the thread pool for long operations). Complex and easy to misuse β€” prefer Task.Run.
  • new Thread(action) β€” creates a dedicated OS thread. High overhead (1 MB stack allocation). Use only when you need explicit thread control (setting thread name for diagnostics, IsBackground flag, custom stack size, STA apartment for COM interop).
// CPU-bound work β€” offload to thread pool
var result = await Task.Run(() => ComputeHeavyResult(data));

// Long-running dedicated work (background service loop)
// Option 1: Task with LongRunning
var task = Task.Factory.StartNew(() => LongRunningLoop(ct),
    ct, TaskCreationOptions.LongRunning, TaskScheduler.Default);

// Option 2: Explicit thread (when you need thread-level control)
var thread = new Thread(() => LongRunningLoop(ct)) {
    Name = "BackgroundProcessor",
    IsBackground = true,   // doesn't prevent process exit
    Priority = ThreadPriority.BelowNormal,
};
thread.Start();

// ASP.NET Core recommendation:
// Use IHostedService / BackgroundService for background work
// Use Task.Run sparingly β€” prefer genuinely async I/O (no thread blocking)
7What is the Parallel class and PLINQ? When do they improve performance?

The Parallel class and PLINQ (Parallel LINQ) enable data parallelism β€” splitting work across multiple CPU cores. They're for CPU-bound work that can be decomposed into independent units.

// Parallel.ForEach β€” process items in parallel
var options = new ParallelOptions {
    MaxDegreeOfParallelism = Environment.ProcessorCount,
    CancellationToken = ct,
};
Parallel.ForEach(items, options, item => ProcessItem(item));

// Parallel.For β€” index-based parallel loop
Parallel.For(0, data.Length, i => {
    result[i] = Compute(data[i]);
});

// PLINQ β€” parallel LINQ queries
var expensive = data
    .AsParallel()
    .WithDegreeOfParallelism(4)
    .WithCancellation(ct)
    .Where(item => IsExpensiveCheck(item))  // parallel filter
    .Select(item => Transform(item))        // parallel transform
    .ToList();

// Preserve order (at cost of reduced parallelism)
var ordered = data.AsParallel().AsOrdered().Select(Transform).ToList();

When parallel DOESN'T help: I/O-bound work (use async/await instead β€” parallelism just blocks multiple threads), very small datasets (thread overhead exceeds benefit), work that requires heavy synchronisation (locks negate parallelism gains), non-CPU-bound bottlenecks (the bottleneck is elsewhere).

When it does help: CPU-bound transformations on large datasets, image processing, scientific computation, ML preprocessing β€” where each item is independent and takes meaningful CPU time.

8What are common async anti-patterns in C#? How do you avoid them?
// ❌ Async void β€” exceptions unobservable, hard to test, avoid except for event handlers
async void DoSomething() { await Task.Delay(100); throw new Exception(); } // silently swallowed

// βœ… Return Task instead
async Task DoSomethingAsync() { await Task.Delay(100); }

// ❌ .Result / .Wait() β€” can deadlock in context-aware environments
var data = GetDataAsync().Result; // blocks thread pool thread, risks deadlock

// βœ… Always await
var data = await GetDataAsync();

// ❌ async in constructors β€” not supported
class Service { public Service() { await InitAsync(); } } // compile error

// βœ… Factory pattern or lazy init
class Service {
    private Service() { }
    public static async Task<Service> CreateAsync() {
        var svc = new Service();
        await svc.InitAsync();
        return svc;
    }
}

// ❌ Unnecessary Task.Run wrapping sync code in async method
async Task<int> GetCountAsync() => await Task.Run(() => _list.Count); // wasteful

// βœ… Return synchronously when no real async work
Task<int> GetCountAsync() => Task.FromResult(_list.Count);

// ❌ Fire-and-forget without handling exceptions
_ = DoWorkAsync(); // exceptions silently swallowed

// βœ… Proper fire-and-forget
_ = DoWorkAsync().ContinueWith(t => logger.LogError(t.Exception, "Failed"),
    TaskContinuationOptions.OnlyOnFaulted);

// ❌ Sequential awaits when parallel is possible
var a = await GetA();
var b = await GetB(); // waits for A before even starting B

// βœ… Parallel awaits
var (a, b) = await (GetA(), GetB()); // C# tuple + Task.WhenAll pattern
// or:
var taskA = GetA();
var taskB = GetB();
await Task.WhenAll(taskA, taskB);
var a = await taskA; var b = await taskB;
9What is IAsyncEnumerable<T>? How does it enable streaming data processing?

IAsyncEnumerable<T> enables async iteration β€” producing elements one at a time with an async operation between each. Unlike returning a Task<List<T>> (which waits for all items before returning), IAsyncEnumerable<T> yields each item as soon as it's ready.

// Producer β€” async iterator method
async IAsyncEnumerable<Order> GetOrdersStreamAsync(
    [EnumeratorCancellation] CancellationToken ct = default) {

    await using var connection = await _db.OpenAsync(ct);
    await using var reader = await connection.ExecuteReaderAsync(
        "SELECT * FROM Orders ORDER BY CreatedAt", ct);

    while (await reader.ReadAsync(ct)) {
        yield return new Order {
            Id    = reader.GetGuid(0),
            Total = reader.GetDecimal(1),
        };
        // Each row yielded as soon as available β€” no buffering all rows in memory
    }
}

// Consumer β€” process as items arrive
await foreach (var order in GetOrdersStreamAsync(ct)) {
    await ProcessOrderAsync(order, ct);
    Console.WriteLine($"Processed {order.Id}");
}

// Real-world: large file processing, database streaming, API pagination
async IAsyncEnumerable<T> PaginateAsync<T>(
    Func<int, Task<Page<T>>> fetchPage,
    [EnumeratorCancellation] CancellationToken ct = default) {

    int pageNum = 1;
    Page<T> page;
    do {
        page = await fetchPage(pageNum++);
        foreach (var item in page.Items)
            yield return item;
    } while (page.HasNextPage && !ct.IsCancellationRequested);
}

LINQ now supports async enumerables via the System.Linq.Async package (or built-in in .NET): await source.Where(...).Select(...).ToListAsync().

LINQ & Collections

6 questions
1How does LINQ work internally? Explain deferred execution and the difference between IEnumerable and IQueryable.

Deferred execution β€” LINQ operators like Where, Select, OrderBy don't execute immediately. They build a query pipeline (chain of iterators). Execution happens when the result is enumerated (foreach, ToList(), Count(), First()).

var query = numbers
    .Where(n => { Console.WriteLine($"Filter: {n}"); return n > 5; })
    .Select(n => { Console.WriteLine($"Map: {n}"); return n * 2; });
// Nothing printed yet β€” query not executed

foreach (var n in query) Console.WriteLine($"Result: {n}"); // now executes
// Filter: 1, Filter: 2, ..., Filter: 6, Map: 6, Result: 12, ...

// Force immediate execution β€” materialise to collection
var list = query.ToList(); // executes and stores all results

IEnumerable<T> vs IQueryable<T>:

  • IEnumerable<T> β€” in-memory, pull-based, lazy. LINQ operators use delegates (execute in .NET). All filtering/projection runs in .NET code on data already loaded.
  • IQueryable<T> β€” represents a queryable data source. LINQ operators build an expression tree that the provider (EF Core, LINQ to SQL) translates to the native query language (SQL). Only the final query runs on the database, and only selected columns/rows are transferred.
// IQueryable β€” EF Core translates to SQL
IQueryable<User> query = dbContext.Users
    .Where(u => u.Age > 18)    // becomes: WHERE Age > 18
    .Select(u => new { u.Id, u.Name }); // becomes: SELECT Id, Name
// Only runs when materialised: query.ToListAsync()

// Common mistake β€” switching to IEnumerable prematurely
var bad = dbContext.Users
    .AsEnumerable()            // loads ALL users into memory!
    .Where(u => u.Age > 18);  // filters in .NET β€” too late
2What are the most common LINQ performance pitfalls and how do you avoid them?
// ❌ Multiple enumeration β€” executes the pipeline multiple times
var expensive = GetExpensiveQuery();
int count = expensive.Count();   // enumerates once
var first = expensive.First();   // enumerates again
// βœ… Materialise once:
var list = GetExpensiveQuery().ToList();

// ❌ .Any() vs .Count() > 0
if (list.Count() > 0) { }    // Count() enumerates entire sequence
// βœ… Any() short-circuits on first match:
if (list.Any()) { }

// ❌ Using FirstOrDefault without ordering β€” non-deterministic
var user = dbContext.Users.FirstOrDefault(u => u.IsAdmin);
// βœ… OrderBy for determinism when it matters
var user = dbContext.Users.Where(u => u.IsAdmin).OrderBy(u => u.Id).FirstOrDefault();

// ❌ Select then Where β€” processes all items before filtering
var result = items.Select(Expensive).Where(x => x.IsValid);
// βœ… Where then Select β€” filter first, then transform remaining
var result = items.Where(x => x.CanBeValid).Select(Expensive);

// ❌ Nested LINQ causing N+1 style problems
var result = orders.Select(o => new {
    o.Id,
    ItemCount = o.Items.Count(), // enumerates Items for each order
}).ToList();
// βœ… Project what you need in one pass

// ❌ Contains on List β€” O(n) search
var found = users.Where(u => validIds.Contains(u.Id)); // O(n*m)
// βœ… HashSet.Contains β€” O(1)
var validSet = new HashSet<int>(validIds);
var found = users.Where(u => validSet.Contains(u.Id));

// LINQ-specific: avoid closures over large objects in Where/Select
// β€” the lambda keeps the object alive as long as the query exists
3What is the difference between Dictionary, ConcurrentDictionary, ImmutableDictionary, and FrozenDictionary?
  • Dictionary<K,V> β€” fast hash map, not thread-safe. For single-threaded or externally synchronised use.
  • ConcurrentDictionary<K,V> β€” thread-safe via fine-grained locking (lock per bucket). Optimised for concurrent reads and writes. Use for shared caches and multi-threaded counters. Key API: GetOrAdd, AddOrUpdate, TryUpdate.
  • ImmutableDictionary<K,V> β€” truly immutable, thread-safe by nature. Modifications return a new instance. Internally uses a balanced tree (HAMT) β€” O(log n) operations vs O(1) for Dictionary. Use for functional programming patterns and persistent data structures.
  • FrozenDictionary<K,V> β€” (.NET 8+) read-only dictionary optimised for lookup speed. Build once, read many times. Faster than Dictionary for lookups due to perfect hashing and cache-friendly layout. Ideal for lookup tables loaded at startup.
// ConcurrentDictionary β€” thread-safe cache
var cache = new ConcurrentDictionary<string, User>();
var user = cache.GetOrAdd(userId, id => LoadUser(id)); // atomic get-or-compute

// Beware: factory in GetOrAdd may execute multiple times under contention
// Use AddOrUpdate for guaranteed single execution (but more complex)

// FrozenDictionary β€” startup lookup table (e.g., country codes)
var countryCodes = new Dictionary<string, string> { {"US","United States"}, ... }
    .ToFrozenDictionary(); // optimises for reads

// Typical lookup performance:
// Dictionary: ~20ns
// FrozenDictionary: ~8ns (perfect hash, better cache locality)
// ConcurrentDictionary: ~30ns (volatile reads for thread safety)
// ImmutableDictionary: ~100ns (tree traversal)
4How do you implement custom LINQ operators and when is it useful?

Custom LINQ operators are extension methods on IEnumerable<T> (or IQueryable<T>) that compose with existing LINQ chains. They enable reusable, readable domain-specific query building.

// Custom LINQ operators via extension methods
public static class LinqExtensions {
    // Batch β€” yield items in groups
    public static IEnumerable<IEnumerable<T>> Batch<T>(
        this IEnumerable<T> source, int size) {
        using var enumerator = source.GetEnumerator();
        while (enumerator.MoveNext()) {
            yield return GetBatch(enumerator, size);
        }
        static IEnumerable<T> GetBatch(IEnumerator<T> e, int size) {
            yield return e.Current;
            for (int i = 1; i < size && e.MoveNext(); i++)
                yield return e.Current;
        }
    }

    // DistinctBy β€” distinct based on a key (standard in .NET 6+)
    public static IEnumerable<T> DistinctBy<T, TKey>(
        this IEnumerable<T> source, Func<T, TKey> keySelector) {
        var seen = new HashSet<TKey>();
        foreach (var item in source)
            if (seen.Add(keySelector(item)))
                yield return item;
    }

    // Domain-specific: filter active orders
    public static IQueryable<Order> Active(this IQueryable<Order> query)
        => query.Where(o => o.Status == OrderStatus.Active && !o.IsDeleted);

    public static IQueryable<Order> ForCustomer(
        this IQueryable<Order> query, Guid customerId)
        => query.Where(o => o.CustomerId == customerId);
}

// Usage β€” readable, reusable query composition
var orders = await dbContext.Orders
    .Active()
    .ForCustomer(customerId)
    .OrderByDescending(o => o.CreatedAt)
    .Take(10)
    .ToListAsync();
5When do you choose List<T> vs LinkedList<T> vs Queue<T> vs Stack<T>?
  • List<T> β€” dynamic array (contiguous memory). O(1) random access by index. O(1) amortised append. O(n) insert/remove at arbitrary position. Best for most scenarios β€” cache-friendly due to contiguous layout.
  • LinkedList<T> β€” doubly linked list. O(1) insert/remove at known node (no shifting). O(n) random access. High memory overhead (object per node, GC pressure). Useful for frequently-modified ordered sequences where you hold node references.
  • Queue<T> β€” FIFO. O(1) Enqueue (add to back) and Dequeue (remove from front). Use for task queues, BFS algorithms, producer-consumer patterns.
  • Stack<T> β€” LIFO. O(1) Push and Pop. Use for undo/redo, DFS algorithms, parsing nested structures.
  • SortedList<K,V> β€” sorted by key, binary search O(log n). Lower memory than SortedDictionary, slower inserts. Good for read-heavy sorted data.
  • PriorityQueue<TElement, TPriority> β€” (.NET 6+) min-heap. O(log n) enqueue/dequeue. Use for Dijkstra, task scheduling, merge sorted streams.
// Benchmark mental model:
// Access by index: List = O(1), LinkedList = O(n)
// Insert at start: List = O(n), LinkedList = O(1) with node ref
// Memory per element: List = 4/8 bytes, LinkedList = ~40 bytes (object overhead)
// Cache performance: List wins (contiguous), LinkedList loses (scattered nodes)

// Priority queue example
var pq = new PriorityQueue<Task, int>();
pq.Enqueue(lowPriorityTask, 10);
pq.Enqueue(highPriorityTask, 1);
var next = pq.Dequeue(); // returns highPriorityTask (priority 1)
6How do you implement IEqualityComparer<T> and IComparer<T>? When are they needed?
// IEqualityComparer<T> β€” custom equality for Dictionary, HashSet, LINQ GroupBy
class UserEmailComparer : IEqualityComparer<User> {
    public bool Equals(User? x, User? y)
        => string.Equals(x?.Email, y?.Email, StringComparison.OrdinalIgnoreCase);

    public int GetHashCode(User obj)
        => obj.Email?.ToLowerInvariant().GetHashCode() ?? 0;
}

// Usage
var uniqueByEmail = users.Distinct(new UserEmailComparer()).ToList();
var userByEmail = new Dictionary<User, string>(new UserEmailComparer());
var grouped = users.GroupBy(u => u, new UserEmailComparer());

// IComparer<T> β€” custom ordering for SortedSet, List.Sort, OrderBy
class UserByAgeDescComparer : IComparer<User> {
    public int Compare(User? x, User? y)
        => Comparer<int>.Default.Compare(y?.Age ?? 0, x?.Age ?? 0); // descending
}

var sortedSet = new SortedSet<User>(new UserByAgeDescComparer());
list.Sort(new UserByAgeDescComparer());

// C# 10+: static lambda comparers (no class needed)
var sortedByName = users.Order(Comparer<User>.Create((a, b) =>
    string.Compare(a.Name, b.Name, StringComparison.Ordinal)));

// When needed:
// - Comparing by non-default criteria (case-insensitive, specific field)
// - Type doesn't implement IComparable / IEquatable (third-party types)
// - Multiple different comparison strategies for the same type

ASP.NET Core

9 questions
1How does the ASP.NET Core middleware pipeline work? How do you write custom middleware?

ASP.NET Core processes requests through a middleware pipeline β€” a chain of components, each of which can inspect/modify the request, call the next component, and then inspect/modify the response on the way back. Order matters β€” middleware executes in the order it's registered.

// Middleware ordering in Program.cs
var app = builder.Build();
app.UseExceptionHandler("/error");      // 1st β€” catches exceptions from all below
app.UseHttpsRedirection();              // 2nd
app.UseStaticFiles();                   // 3rd β€” short-circuits for static files
app.UseRouting();                       // 4th
app.UseAuthentication();                // 5th β€” populate HttpContext.User
app.UseAuthorization();                 // 6th β€” enforce policies
app.MapControllers();                   // 7th β€” route to controllers

// Custom middleware β€” class-based (recommended for complex middleware)
public class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger) {
    public async Task InvokeAsync(HttpContext context) {
        var sw = Stopwatch.StartNew();
        try {
            await next(context); // call the next middleware in the pipeline
        } finally {
            sw.Stop();
            logger.LogInformation(
                "{Method} {Path} {StatusCode} {Elapsed}ms",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                sw.ElapsedMilliseconds);
        }
    }
}

// Register custom middleware
app.UseMiddleware<RequestTimingMiddleware>();

// Inline middleware β€” for simple cases
app.Use(async (context, next) => {
    context.Response.Headers.Append("X-Custom-Header", "value");
    await next(context);
});

// Short-circuit β€” don't call next
app.Run(async context => {
    await context.Response.WriteAsync("Terminal middleware");
});
2How does dependency injection work in ASP.NET Core? Explain lifetimes: Singleton, Scoped, and Transient.

ASP.NET Core has a built-in DI container. Services are registered in Program.cs with a lifetime that controls how instances are created and shared.

  • Singleton β€” one instance for the entire application lifetime. Shared across all requests and threads. Use for: stateless services, caches, expensive-to-create resources (HttpClient via IHttpClientFactory), configuration objects.
  • Scoped β€” one instance per HTTP request (one per scope). A new scope is created for each request. Use for: DbContext, Unit of Work, services that hold request-specific state.
  • Transient β€” a new instance every time it's resolved from the container. Use for: lightweight, stateless services. Be careful of allocations in hot paths.
// Registration
builder.Services.AddSingleton<IEmailService, SendGridEmailService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IValidator<Order>, OrderValidator>();

// Keyed services (ASP.NET Core 8+) β€” multiple implementations of same interface
builder.Services.AddKeyedSingleton<IStorage, LocalStorage>("local");
builder.Services.AddKeyedSingleton<IStorage, AzureBlobStorage>("azure");

class UploadService([FromKeyedServices("azure")] IStorage storage) { }

// Captive dependency anti-pattern β€” causes bugs
// Singleton captures Scoped β†’ Scoped lives as long as Singleton (wrong!)
builder.Services.AddSingleton<BadService>(); // captures scoped DbContext β†’ bug

// Runtime validation (Development only)
builder.Services.AddMvc().AddControllersAsServices();
// Or: builder.Host.UseDefaultServiceProvider(options =>
//     options.ValidateScopes = true); // throws on captive deps
3How do you implement authentication and authorisation in ASP.NET Core? Cover JWT and policy-based auth.
// JWT Authentication setup
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.TokenValidationParameters = new TokenValidationParameters {
            ValidateIssuer           = true,
            ValidateAudience         = true,
            ValidateLifetime         = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer              = builder.Configuration["Jwt:Issuer"],
            ValidAudience            = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey         = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
        };
    });

// Policy-based authorisation
builder.Services.AddAuthorization(options => {
    options.AddPolicy("SeniorEmployee", policy => policy
        .RequireAuthenticatedUser()
        .RequireClaim("department", "engineering")
        .RequireAssertion(ctx =>
            ctx.User.FindFirstValue("yearsExperience") is string y && int.Parse(y) >= 5));

    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});

// Custom requirement + handler
record MinimumAgeRequirement(int MinimumAge) : IAuthorizationRequirement;

class MinimumAgeHandler(IHttpContextAccessor httpCtx)
    : AuthorizationHandler<MinimumAgeRequirement> {
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement) {
        var ageClaim = context.User.FindFirst("age");
        if (ageClaim != null && int.Parse(ageClaim.Value) >= requirement.MinimumAge)
            context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

// Apply policies
[Authorize(Policy = "SeniorEmployee")]
[HttpGet("sensitive")]
public IActionResult SensitiveEndpoint() => Ok();

// Minimal API
app.MapGet("/admin", () => "Admin only").RequireAuthorization("AdminOnly");
4What are Minimal APIs in ASP.NET Core? How do they compare to controller-based APIs?

Minimal APIs (ASP.NET Core 6+) define HTTP endpoints with minimal ceremony β€” no controllers, no action attributes. They have lower overhead per request and faster startup, making them ideal for microservices and serverless.

// Minimal API β€” concise, functional style
var app = builder.Build();

app.MapGet("/users/{id:int}", async (int id, IUserService svc, CancellationToken ct)
    => await svc.GetByIdAsync(id, ct) is { } user ? Results.Ok(user) : Results.NotFound())
    .WithName("GetUser")
    .WithOpenApi()
    .Produces<User>(200)
    .ProducesProblem(404)
    .RequireAuthorization();

// Group routes β€” cleaner organisation for larger APIs
var usersApi = app.MapGroup("/api/users").RequireAuthorization().WithOpenApi();
usersApi.MapGet("/", GetAllUsers);
usersApi.MapGet("/{id:int}", GetUserById);
usersApi.MapPost("/", CreateUser);
usersApi.MapDelete("/{id:int}", DeleteUser).RequireAuthorization("AdminOnly");

// Route handler as static method β€” more testable, no closure allocation
static async Task<IResult> GetUserById(int id, IUserService svc, CancellationToken ct)
    => await svc.GetByIdAsync(id, ct) is { } user ? Results.Ok(user) : Results.NotFound();

Minimal API vs Controllers:

  • Minimal API: less boilerplate, faster startup, slightly better performance per request, ideal for small/focused services.
  • Controllers: better for complex APIs with many actions, built-in model validation via attributes, filters, action results type system, more familiar to large teams.
  • Both support DI, filters, middleware, OpenAPI. Modern .NET teams often mix: Minimal APIs for simple CRUD endpoints, controllers for complex business workflows.
5How does model validation work in ASP.NET Core? Explain the difference between data annotations and IValidatableObject vs FluentValidation.
// Data annotations β€” declarative, simple
class CreateUserRequest {
    [Required, StringLength(100, MinimumLength = 2)]
    public string Name { get; init; } = "";

    [Required, EmailAddress]
    public string Email { get; init; } = "";

    [Range(18, 150)]
    public int Age { get; init; }

    [RegularExpression(@"^\+?[1-9]\d{1,14}$", ErrorMessage = "Invalid phone")]
    public string? Phone { get; init; }
}

// IValidatableObject β€” cross-property validation
class CreateOrderRequest : IValidatableObject {
    public DateTime StartDate { get; init; }
    public DateTime EndDate { get; init; }

    public IEnumerable<ValidationResult> Validate(ValidationContext context) {
        if (EndDate <= StartDate)
            yield return new ValidationResult(
                "EndDate must be after StartDate",
                new[] { nameof(EndDate) });
    }
}

// FluentValidation β€” complex rules, external validation logic
class CreateUserValidator : AbstractValidator<CreateUserRequest> {
    public CreateUserValidator(IUserRepository repo) {
        RuleFor(x => x.Name).NotEmpty().Length(2, 100).WithMessage("Name must be 2-100 chars");
        RuleFor(x => x.Email).NotEmpty().EmailAddress()
            .MustAsync(async (email, ct) => !await repo.EmailExistsAsync(email))
            .WithMessage("Email already registered");
        RuleFor(x => x.Age).InclusiveBetween(18, 150);
        When(x => x.Phone != null, () => {
            RuleFor(x => x.Phone).Matches(@"^\+?[1-9]\d{1,14}$");
        });
    }
}

// Register FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserValidator>();
builder.Services.AddFluentValidationAutoValidation();

Use data annotations for simple constraints (required, length, range). Use IValidatableObject for cross-property validation within a model. Use FluentValidation for complex rules, async validation (database uniqueness checks), conditional rules, and separation of validation logic from model classes β€” preferred in enterprise applications.

6How do you implement caching in ASP.NET Core? Compare in-memory, distributed, and output caching.
// 1. IMemoryCache β€” single server, in-process
builder.Services.AddMemoryCache();

class ProductService(IMemoryCache cache, IProductRepository repo) {
    async Task<Product?> GetAsync(int id, CancellationToken ct) {
        return await cache.GetOrCreateAsync($"product:{id}", async entry => {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            entry.SlidingExpiration = TimeSpan.FromMinutes(2);
            entry.Size = 1;
            return await repo.FindAsync(id, ct);
        });
    }
}

// 2. IDistributedCache β€” multi-server (Redis, SQL Server)
builder.Services.AddStackExchangeRedisCache(options => {
    options.Configuration = builder.Configuration["Redis:ConnectionString"];
});

async Task<User?> GetUserAsync(string id, IDistributedCache cache, CancellationToken ct) {
    var key = $"user:{id}";
    var cached = await cache.GetStringAsync(key, ct);
    if (cached != null) return JsonSerializer.Deserialize<User>(cached);

    var user = await LoadUserFromDb(id);
    if (user != null)
        await cache.SetStringAsync(key, JsonSerializer.Serialize(user),
            new DistributedCacheEntryOptions {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
            }, ct);
    return user;
}

// 3. Output caching (ASP.NET Core 7+) β€” cache HTTP responses
builder.Services.AddOutputCache(options => {
    options.AddPolicy("Products", policy => policy
        .Expire(TimeSpan.FromMinutes(5))
        .Tag("products"));
});
app.UseOutputCache();

app.MapGet("/products", async (IProductService svc) => await svc.GetAllAsync())
    .CacheOutput("Products");

// Invalidate on update
app.MapPost("/products", async (Product p, IOutputCacheStore store, CancellationToken ct) => {
    await SaveAsync(p);
    await store.EvictByTagAsync("products", ct); // invalidate cached responses
    return Results.Created($"/products/{p.Id}", p);
});
7What is the Options pattern in ASP.NET Core? How do you bind and validate configuration?
// appsettings.json
{
  "Email": {
    "SmtpHost": "smtp.sendgrid.net",
    "Port": 587,
    "ApiKey": "${EMAIL_API_KEY}",
    "FromAddress": "no-reply@myapp.com"
  }
}

// Options class with validation
class EmailOptions {
    public const string SectionName = "Email";

    [Required, MinLength(1)]
    public string SmtpHost { get; init; } = "";

    [Range(1, 65535)]
    public int Port { get; init; } = 587;

    [Required]
    public string ApiKey  { get; init; } = "";

    [Required, EmailAddress]
    public string FromAddress { get; init; } = "";
}

// Registration with validation at startup (fail fast)
builder.Services.AddOptions<EmailOptions>()
    .BindConfiguration(EmailOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart(); // throw on app startup if invalid β€” don't wait for first use

// Three interfaces:
// IOptions<T>         β€” singleton, reads config once at startup (no hot reload)
// IOptionsSnapshot<T> β€” scoped, re-reads each request (supports hot reload, can't inject in singleton)
// IOptionsMonitor<T>  β€” singleton, hot reload with change notification

class EmailService(IOptions<EmailOptions> options) {
    // options.Value is the configured EmailOptions
    private readonly EmailOptions _opts = options.Value;
}

// Hot reload with IOptionsMonitor
class ConfigMonitor(IOptionsMonitor<EmailOptions> monitor) {
    public EmailOptions Current => monitor.CurrentValue;
    // monitor.OnChange(opts => logger.LogInformation("Config changed"));
}
8How does HTTP client management work in ASP.NET Core? What is IHttpClientFactory and why is it important?

Creating a new HttpClient per request is a well-known mistake β€” it exhausts socket connections due to TIME_WAIT states even after disposal. HttpClient is designed to be reused, but singleton HttpClient doesn't respect DNS changes.

IHttpClientFactory solves both problems by pooling HttpMessageHandler instances with lifecycle management (cycle handlers after 2 minutes to pick up DNS changes).

// Named client
builder.Services.AddHttpClient("github", client => {
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
});

// Typed client β€” strongly typed wrapper
class GitHubService(HttpClient client) {
    public async Task<GitHubUser?> GetUserAsync(string login, CancellationToken ct)
        => await client.GetFromJsonAsync<GitHubUser>($"users/{login}", ct);
}
builder.Services.AddHttpClient<GitHubService>(client => {
    client.BaseAddress = new Uri("https://api.github.com/");
});

// Resilience with Microsoft.Extensions.Http.Resilience (.NET 8)
builder.Services.AddHttpClient<GitHubService>()
    .AddStandardResilienceHandler(); // retry, circuit breaker, hedging, timeout

// Custom resilience pipeline
builder.Services.AddHttpClient<PaymentService>()
    .AddResilienceHandler("payment-pipeline", builder => {
        builder.AddRetry(new HttpRetryStrategyOptions {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromSeconds(1),
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true,
        });
        builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions {
            SamplingDuration  = TimeSpan.FromSeconds(10),
            FailureRatio      = 0.5,
            MinimumThroughput = 10,
            BreakDuration     = TimeSpan.FromSeconds(30),
        });
        builder.AddTimeout(TimeSpan.FromSeconds(5));
    });
9What are filters in ASP.NET Core? Explain the filter pipeline and the different filter types.

Filters run before/after specific stages of the MVC pipeline. They are a way to encapsulate cross-cutting concerns (logging, validation, exception handling, caching) without cluttering action methods.

Filter execution order: Authorization β†’ Resource β†’ Action β†’ Exception β†’ Result

// Action filter β€” runs before/after action method
class ValidateModelAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext context) {
        if (!context.ModelState.IsValid)
            context.Result = new BadRequestObjectResult(context.ModelState);
    }
}

// Exception filter β€” handles unhandled exceptions
class ApiExceptionFilterAttribute : ExceptionFilterAttribute {
    public override void OnException(ExceptionContext context) {
        var problem = context.Exception switch {
            NotFoundException e => new ProblemDetails { Status = 404, Detail = e.Message },
            ValidationException e => new ProblemDetails { Status = 422, Detail = e.Message },
            _ => new ProblemDetails { Status = 500, Detail = "An error occurred" },
        };
        context.Result = new ObjectResult(problem) { StatusCode = problem.Status };
        context.ExceptionHandled = true;
    }
}

// Resource filter β€” runs before model binding (useful for caching)
class CacheResourceFilter(IOutputCacheStore cache) : IResourceFilter {
    public void OnResourceExecuting(ResourceExecutingContext context) { /* check cache */ }
    public void OnResourceExecuted(ResourceExecutedContext context) { /* store in cache */ }
}

// Register globally
builder.Services.AddControllers(options => {
    options.Filters.Add<ValidateModelAttribute>();
    options.Filters.Add<ApiExceptionFilterAttribute>();
});

Entity Framework Core

8 questions
1How does EF Core change tracking work? What are the different entity states?

EF Core's change tracker monitors entities loaded from the database and detects modifications. On SaveChanges(), it generates the appropriate INSERT/UPDATE/DELETE SQL for changed entities.

Entity states:

  • Detached β€” not tracked by any context (new objects, or after AsNoTracking()).
  • Added β€” new entity, will be INSERTed.
  • Unchanged β€” loaded from DB, no changes detected.
  • Modified β€” properties changed since loading, will be UPDATEd.
  • Deleted β€” marked for deletion, will be DELETEd.
// Change tracking in action
var product = await dbContext.Products.FindAsync(1); // Unchanged
product.Price = 99.99m; // Modified β€” EF detects the change automatically
product.Name = "New Name"; // Also Modified
await dbContext.SaveChangesAsync(); // UPDATE Products SET Price=99.99, Name='New Name' WHERE Id=1

// Check entity state
var entry = dbContext.Entry(product);
Console.WriteLine(entry.State); // EntityState.Modified

// Manually set state β€” useful when attaching detached entities
var detachedProduct = new Product { Id = 1, Price = 150 };
dbContext.Attach(detachedProduct);
dbContext.Entry(detachedProduct).Property(p => p.Price).IsModified = true;
await dbContext.SaveChangesAsync(); // UPDATE only Price column

// AsNoTracking β€” read-only queries (faster, no change detection overhead)
var products = await dbContext.Products
    .AsNoTracking()      // entities are Detached β€” not tracked
    .Where(p => p.IsActive)
    .ToListAsync();
// Cannot SaveChanges() on these β€” use for read-only APIs

// AsNoTrackingWithIdentityResolution (EF Core 5+)
// β€” no tracking, but identity map prevents duplicate instances
var orders = await dbContext.Orders
    .AsNoTrackingWithIdentityResolution()
    .Include(o => o.Customer)
    .ToListAsync();
2What is the N+1 query problem in EF Core? How do you solve it with eager, explicit, and lazy loading?
// N+1 problem β€” one query for orders, then one per order for customer
var orders = await dbContext.Orders.ToListAsync();  // 1 query
foreach (var order in orders) {
    Console.WriteLine(order.Customer.Name); // N additional queries!
}

// Solution 1: Eager loading with Include
var orders = await dbContext.Orders
    .Include(o => o.Customer)                      // JOIN in single query
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)               // nested navigation
    .ToListAsync();

// Solution 2: Split queries (for multi-collection includes β€” avoids cartesian explosion)
var orders = await dbContext.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
    .AsSplitQuery()   // generates separate SQL queries β€” no duplicated rows
    .ToListAsync();

// Solution 3: Explicit loading β€” load navigations on demand
var order = await dbContext.Orders.FindAsync(orderId);
await dbContext.Entry(order).Reference(o => o.Customer).LoadAsync();
await dbContext.Entry(order).Collection(o => o.Items).LoadAsync();

// Solution 4: Projection β€” only fetch what you need
var summaries = await dbContext.Orders
    .Select(o => new {
        o.Id,
        CustomerName = o.Customer.Name,  // EF generates a JOIN automatically
        ItemCount = o.Items.Count(),
    })
    .ToListAsync();

EF Core 8+ detects potential N+1 patterns and logs warnings. Enable query logging in development: optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information).EnableSensitiveDataLogging().

3How do you handle database migrations in EF Core in a production environment?
// Create and apply migrations
dotnet ef migrations add AddProductCategory --project DataLayer --startup-project Api
dotnet ef database update  // dev only

// Production β€” generate SQL script, review, run manually
dotnet ef migrations script --idempotent --output migration.sql
// --idempotent: uses IF NOT EXISTS checks, safe to run multiple times

// Apply at app startup (small apps / microservices)
using (var scope = app.Services.CreateScope()) {
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync(); // applies pending migrations
}

// Best practice for large apps:
// 1. Never run migrations automatically in production with many instances
// 2. Run as a one-off migration job in CI/CD before deploying new code
// 3. Use --idempotent script reviewed and approved before execution
// 4. Use expand-contract pattern for breaking schema changes

// Expand-contract for zero-downtime schema changes:
// Step 1: Add new column (nullable, with default) β€” deploy
// Step 2: Backfill data β€” run script
// Step 3: Make column non-nullable β€” deploy
// Step 4: Remove old column β€” deploy (after all app versions use new schema)
4What are owned entities, value objects, and complex types in EF Core?
// Value Object in DDD β€” mapped as EF Core owned entity
public record Address(string Street, string City, string PostalCode, string Country);

public class Customer {
    public int Id { get; init; }
    public string Name { get; init; } = "";
    public Address ShippingAddress { get; init; } = null!;
    public Address? BillingAddress  { get; init; }
}

// Configuration β€” owned entities (stored in same table by default)
protected override void OnModelCreating(ModelBuilder modelBuilder) {
    modelBuilder.Entity<Customer>(builder => {
        builder.OwnsOne(c => c.ShippingAddress, addr => {
            addr.Property(a => a.Street).HasColumnName("ShippingStreet").IsRequired();
            addr.Property(a => a.City).HasColumnName("ShippingCity");
            addr.Property(a => a.PostalCode).HasColumnName("ShippingPostalCode");
        });
        builder.OwnsOne(c => c.BillingAddress, addr => {
            addr.Property(a => a.Street).HasColumnName("BillingStreet");
            // billing is nullable β€” all columns nullable in DB
        });
    });
}

// EF Core 8 β€” Complex Types (no identity, no table split)
[ComplexType]  // simpler than owned entities for pure value objects
public record Money(decimal Amount, string Currency);

// JSON columns (EF Core 7+) β€” store complex types as JSON
modelBuilder.Entity<Order>().OwnsMany(o => o.Tags, b => b.ToJson());
modelBuilder.Entity<Order>().OwnsOne(o => o.Metadata, b => b.ToJson());
5How do you implement the Repository and Unit of Work patterns with EF Core?

Repository pattern abstracts data access behind an interface β€” makes domain logic testable and swappable. Unit of Work coordinates writing to multiple repositories as a single transaction.

// Generic repository interface
interface IRepository<T> where T : class {
    Task<T?> FindAsync(object id, CancellationToken ct = default);
    Task<IReadOnlyList<T>> ListAsync(CancellationToken ct = default);
    Task<IReadOnlyList<T>> ListAsync(Expression<Func<T, bool>> filter, CancellationToken ct = default);
    Task AddAsync(T entity, CancellationToken ct = default);
    void Update(T entity);
    void Remove(T entity);
}

// EF Core implementation
class Repository<T>(AppDbContext context) : IRepository<T> where T : class {
    protected readonly DbSet<T> DbSet = context.Set<T>();

    public Task<T?> FindAsync(object id, CancellationToken ct)
        => DbSet.FindAsync(new[] { id }, ct).AsTask();

    public Task<IReadOnlyList<T>> ListAsync(CancellationToken ct)
        => DbSet.AsNoTracking().ToListAsync(ct)
               .ContinueWith(t => (IReadOnlyList<T>)t.Result);

    public async Task AddAsync(T entity, CancellationToken ct)
        => await DbSet.AddAsync(entity, ct);

    public void Update(T entity) => context.Entry(entity).State = EntityState.Modified;
    public void Remove(T entity) => DbSet.Remove(entity);
}

// Unit of Work β€” DbContext IS a UoW (no need to wrap it in most cases)
interface IUnitOfWork : IDisposable {
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    Task<int> SaveChangesAsync(CancellationToken ct = default);
}

class UnitOfWork(AppDbContext context) : IUnitOfWork {
    public IOrderRepository Orders { get; } = new OrderRepository(context);
    public ICustomerRepository Customers { get; } = new CustomerRepository(context);
    public Task<int> SaveChangesAsync(CancellationToken ct) => context.SaveChangesAsync(ct);
    public void Dispose() => context.Dispose();
}
6How do you handle concurrency conflicts in EF Core?
// Optimistic concurrency β€” row version token
public class Product {
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }

    [Timestamp]  // auto-generated, incremented on update
    public byte[] RowVersion { get; set; } = [];
}

// EF Core generates: UPDATE ... WHERE Id=1 AND RowVersion=@original
// If row was updated by another user, RowVersion differs β†’ 0 rows affected β†’ exception

try {
    await dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex) {
    var entry = ex.Entries.Single();
    var dbValues = await entry.GetDatabaseValuesAsync(); // current DB state
    if (dbValues == null) {
        // Entity was deleted by another user
        return Conflict("The item was deleted by another user");
    }

    // Strategy: client wins β€” overwrite DB values
    entry.OriginalValues.SetValues(dbValues);
    await dbContext.SaveChangesAsync(); // retry

    // Strategy: database wins β€” discard client changes
    entry.CurrentValues.SetValues(dbValues);
    entry.OriginalValues.SetValues(dbValues);
}

// Alternative: ETag-based concurrency for APIs
// Return ETag header with RowVersion on GET
// Require If-Match header on PUT/PATCH
// Return 412 Precondition Failed if ETag doesn't match
7What are compiled queries in EF Core and when do you use them?

EF Core translates LINQ expression trees to SQL on every execution β€” this parsing/translation has a cost (~1ms for complex queries). Compiled queries cache the translated SQL, eliminating translation overhead on repeated calls.

// Compiled query β€” translated once, cached forever
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
    EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
        ctx.Products
           .AsNoTracking()
           .Include(p => p.Category)
           .FirstOrDefault(p => p.Id == id));

// Usage β€” no translation cost
var product = await GetProductById(dbContext, productId);

// Compiled query with multiple parameters
private static readonly Func<AppDbContext, string, decimal, IAsyncEnumerable<Product>>
    SearchProducts = EF.CompileAsyncQuery(
        (AppDbContext ctx, string name, decimal maxPrice) =>
            ctx.Products.Where(p => p.Name.Contains(name) && p.Price <= maxPrice));

await foreach (var product in SearchProducts(dbContext, "laptop", 999.99m))
    Process(product);

// When to use compiled queries:
// βœ… Hot path queries executed thousands of times per second
// βœ… Queries with significant translation cost (complex projections, multiple includes)
// ❌ Queries with dynamic shape (variable number of includes, dynamic OrderBy)
//    β€” dynamic queries can't be fully compiled
// Note: EF Core 6+ uses query caching internally, reducing the gap with compiled queries
8How do you use raw SQL in EF Core? When should you bypass the ORM?
// FromSqlRaw β€” compose raw SQL with LINQ (returns tracked entities)
var products = await dbContext.Products
    .FromSqlRaw("SELECT * FROM Products WHERE CONTAINS(Name, {0})", searchTerm)
    .Where(p => p.IsActive)       // LINQ composition on top of raw SQL
    .OrderBy(p => p.Name)
    .ToListAsync();

// FromSqlInterpolated β€” safe interpolation (parameterised automatically)
var cutoff = DateTime.UtcNow.AddDays(-30);
var recentOrders = await dbContext.Orders
    .FromSqlInterpolated($"SELECT * FROM Orders WHERE CreatedAt > {cutoff}")
    .ToListAsync();

// ExecuteSqlRawAsync β€” for INSERT/UPDATE/DELETE without tracking
int rowsAffected = await dbContext.Database
    .ExecuteSqlRawAsync(
        "UPDATE Products SET Price = Price * @multiplier WHERE CategoryId = @catId",
        new SqlParameter("multiplier", 1.1),
        new SqlParameter("catId", categoryId));

// Dapper β€” for complex queries where EF LINQ is too cumbersome
using var conn = dbContext.Database.GetDbConnection();
var results = await conn.QueryAsync<ProductSummary>(@"
    SELECT p.Name, c.Name AS CategoryName, COUNT(oi.Id) AS OrderCount
    FROM Products p
    JOIN Categories c ON p.CategoryId = c.Id
    LEFT JOIN OrderItems oi ON p.Id = oi.ProductId
    GROUP BY p.Id, p.Name, c.Name
    HAVING COUNT(oi.Id) > 100
    ORDER BY OrderCount DESC", new { threshold = 100 });

When to bypass EF Core: complex reporting queries with multiple joins/aggregations that LINQ produces poor SQL for, bulk operations (use ExecuteUpdateAsync/ExecuteDeleteAsync in EF Core 7+ or libraries like EFCore.BulkExtensions), stored procedure calls, full-text search, database-specific features.

Design Patterns

8 questions
1What is SOLID and how do you apply each principle in C#?
  • S β€” Single Responsibility: a class has one reason to change. Extract responsibilities into separate classes (a UserService doesn't also handle email sending).
  • O β€” Open/Closed: open for extension, closed for modification. Add behaviour via new classes (new strategy, new handler) rather than modifying existing ones. Enables adding features without regression risk.
  • L β€” Liskov Substitution: subtypes must be substitutable for their base types. A method accepting Animal must work correctly with any Dog extends Animal β€” no surprising behaviour changes.
  • I β€” Interface Segregation: clients shouldn't depend on interfaces they don't use. Many small, focused interfaces over one large "fat" interface.
  • D β€” Dependency Inversion: depend on abstractions (interfaces), not concretions (classes). High-level modules don't depend on low-level modules. Enables DI, mocking, and swappable implementations.
// OCP β€” adding discount types without modifying DiscountCalculator
interface IDiscountStrategy { decimal Calculate(decimal price); }
class PercentageDiscount(decimal percent) : IDiscountStrategy {
    public decimal Calculate(decimal price) => price * (1 - percent / 100);
}
class FixedAmountDiscount(decimal amount) : IDiscountStrategy {
    public decimal Calculate(decimal price) => price - amount;
}
class DiscountCalculator(IDiscountStrategy strategy) {
    public decimal Apply(decimal price) => strategy.Calculate(price);
}

// ISP β€” small focused interfaces
interface IUserReader { Task<User?> FindAsync(int id); }
interface IUserWriter { Task AddAsync(User user); Task UpdateAsync(User user); }
interface IUserRepository : IUserReader, IUserWriter { }

// DIP β€” depend on abstraction, not SqlUserRepository
class UserService(IUserRepository repo) { } // testable, swappable
2What is the CQRS pattern? How do you implement it with MediatR in .NET?

CQRS (Command Query Responsibility Segregation) separates read operations (queries) from write operations (commands) into different models. Commands change state and return no data (or only an ID/status). Queries return data without changing state.

// Command β€” intent to change state
record CreateOrderCommand(Guid CustomerId, List<OrderItemDto> Items) : IRequest<Guid>;

// Command Handler
class CreateOrderHandler(IOrderRepository orders, IUnitOfWork uow)
    : IRequestHandler<CreateOrderCommand, Guid> {

    public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct) {
        var order = Order.Create(cmd.CustomerId, cmd.Items.Select(i => new OrderItem(i)));
        await orders.AddAsync(order, ct);
        await uow.SaveChangesAsync(ct);
        return order.Id;
    }
}

// Query β€” read-only, returns a DTO
record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDto?>;

class GetOrderByIdHandler(IReadDbContext readDb)
    : IRequestHandler<GetOrderByIdQuery, OrderDto?> {

    public async Task<OrderDto?> Handle(GetOrderByIdQuery query, CancellationToken ct)
        => await readDb.Orders
              .AsNoTracking()
              .Where(o => o.Id == query.OrderId)
              .Select(o => new OrderDto(o.Id, o.CustomerId, o.Total, o.Status))
              .FirstOrDefaultAsync(ct);
}

// Pipeline behaviour β€” cross-cutting concerns
class LoggingBehavior<TRequest, TResponse>(ILogger logger)
    : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull {

    public async Task<TResponse> Handle(
        TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct) {
        logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
        var response = await next();
        logger.LogInformation("Handled {Request}", typeof(TRequest).Name);
        return response;
    }
}

// Registration
builder.Services.AddMediatR(cfg => {
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});
3What is the Decorator pattern in C#? How is it used in ASP.NET Core?

The Decorator pattern wraps an object with another object of the same interface, adding behaviour before/after delegating to the wrapped object. Composable and transparent to clients.

// Base interface
interface IOrderService {
    Task<Order?> GetOrderAsync(Guid id, CancellationToken ct);
    Task<Guid> CreateOrderAsync(CreateOrderDto dto, CancellationToken ct);
}

// Core implementation
class OrderService(IOrderRepository repo, IUnitOfWork uow) : IOrderService { ... }

// Caching decorator
class CachedOrderService(IOrderService inner, IMemoryCache cache) : IOrderService {
    public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct)
        => await cache.GetOrCreateAsync($"order:{id}", async _ => await inner.GetOrderAsync(id, ct));

    public Task<Guid> CreateOrderAsync(CreateOrderDto dto, CancellationToken ct)
        => inner.CreateOrderAsync(dto, ct); // passthrough
}

// Logging decorator
class LoggedOrderService(IOrderService inner, ILogger<LoggedOrderService> logger) : IOrderService {
    public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct) {
        logger.LogInformation("Getting order {Id}", id);
        return await inner.GetOrderAsync(id, ct);
    }
    // ...
}

// Register decorators in DI (Scrutor library makes this easy)
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, CachedOrderService>();
builder.Services.Decorate<IOrderService, LoggedOrderService>();
// Resolved chain: LoggedOrderService β†’ CachedOrderService β†’ OrderService
4What is the Specification pattern and how does it improve query code?

The Specification pattern encapsulates query criteria as objects. Specifications can be composed (And, Or, Not) and applied to both in-memory collections and database queries, promoting reuse and testability of business rules.

// Base specification
abstract class Specification<T> {
    public abstract Expression<Func<T, bool>> ToExpression();

    public bool IsSatisfiedBy(T entity) => ToExpression().Compile()(entity);

    public Specification<T> And(Specification<T> other)
        => new AndSpecification<T>(this, other);

    public Specification<T> Or(Specification<T> other)
        => new OrSpecification<T>(this, other);

    public Specification<T> Not() => new NotSpecification<T>(this);
}

// And composition
class AndSpecification<T>(Specification<T> left, Specification<T> right) : Specification<T> {
    public override Expression<Func<T, bool>> ToExpression() {
        var leftExpr  = left.ToExpression();
        var rightExpr = right.ToExpression();
        var param = Expression.Parameter(typeof(T));
        var body  = Expression.AndAlso(
            Expression.Invoke(leftExpr, param),
            Expression.Invoke(rightExpr, param));
        return Expression.Lambda<Func<T, bool>>(body, param);
    }
}

// Domain specifications
class ActiveProductSpec : Specification<Product> {
    public override Expression<Func<Product, bool>> ToExpression()
        => p => p.IsActive && !p.IsDeleted;
}

class PriceRangeSpec(decimal min, decimal max) : Specification<Product> {
    public override Expression<Func<Product, bool>> ToExpression()
        => p => p.Price >= min && p.Price <= max;
}

// Composition
var spec = new ActiveProductSpec().And(new PriceRangeSpec(10, 100));

// Apply to IQueryable (EF Core)
var products = await dbContext.Products
    .Where(spec.ToExpression())
    .ToListAsync();
5What is the Outbox pattern and why is it important for reliable messaging in microservices?

The classic problem: after saving to the database, the application crashes before publishing the event to a message broker. The database update commits but the event is lost β€” other services never know the order was created.

The Outbox pattern writes both the business data and the outgoing message to the same database transaction. A separate background process (outbox processor) reads unsent messages and publishes them to the broker. This guarantees at-least-once delivery.

// Outbox message table
public class OutboxMessage {
    public Guid Id           { get; init; } = Guid.NewGuid();
    public string Type       { get; init; } = ""; // e.g., "OrderCreated"
    public string Content    { get; init; } = ""; // JSON payload
    public DateTime Created  { get; init; } = DateTime.UtcNow;
    public DateTime? Sent    { get; set; }   // null until processed
    public string? Error     { get; set; }
}

// Save order + outbox message in one transaction
async Task CreateOrderAsync(CreateOrderCommand cmd, CancellationToken ct) {
    var order = Order.Create(cmd.CustomerId, cmd.Items);
    var outbox = new OutboxMessage {
        Type    = nameof(OrderCreated),
        Content = JsonSerializer.Serialize(new OrderCreated(order.Id, order.Total)),
    };
    dbContext.Orders.Add(order);
    dbContext.OutboxMessages.Add(outbox); // same transaction
    await dbContext.SaveChangesAsync(ct); // atomic commit
}

// Background outbox processor (IHostedService)
class OutboxProcessor(IServiceScopeFactory scopeFactory, IMessageBus bus) : BackgroundService {
    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        while (!stoppingToken.IsCancellationRequested) {
            using var scope = scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var pending = await db.OutboxMessages
                .Where(m => m.Sent == null)
                .OrderBy(m => m.Created)
                .Take(100)
                .ToListAsync(stoppingToken);

            foreach (var msg in pending) {
                await bus.PublishAsync(msg.Type, msg.Content, stoppingToken);
                msg.Sent = DateTime.UtcNow;
            }
            await db.SaveChangesAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}
6What is Domain-Driven Design (DDD) and how do you implement aggregates in C#?
// Aggregate Root β€” controls all access to its children
public class Order : AggregateRoot {
    private readonly List<OrderItem> _items = [];
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    public Guid CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public Money Total { get; private set; } = Money.Zero;

    private Order() { } // EF Core needs parameterless constructor

    // Factory method β€” enforces invariants on creation
    public static Order Create(Guid customerId) {
        if (customerId == Guid.Empty) throw new ArgumentException("Invalid customer");
        var order = new Order { CustomerId = customerId, Status = OrderStatus.Draft };
        order.AddDomainEvent(new OrderCreated(order.Id, customerId));
        return order;
    }

    // Commands β€” enforce invariants, raise domain events
    public void AddItem(Guid productId, int quantity, Money unitPrice) {
        if (Status != OrderStatus.Draft) throw new InvalidOperationException("Cannot modify non-draft order");
        if (quantity <= 0) throw new ArgumentException("Quantity must be positive");

        var existing = _items.FirstOrDefault(i => i.ProductId == productId);
        if (existing != null)
            existing.IncreaseQuantity(quantity);
        else
            _items.Add(new OrderItem(productId, quantity, unitPrice));

        Total = _items.Aggregate(Money.Zero, (sum, i) => sum.Add(i.LineTotal));
    }

    public void Confirm() {
        if (Status != OrderStatus.Draft) throw new InvalidOperationException("Already confirmed");
        if (!_items.Any()) throw new InvalidOperationException("Cannot confirm empty order");
        Status = OrderStatus.Confirmed;
        AddDomainEvent(new OrderConfirmed(Id, Total));
    }
}

// AggregateRoot base class
abstract class AggregateRoot {
    public Guid Id { get; protected init; } = Guid.NewGuid();
    private readonly List<IDomainEvent> _events = [];
    public IReadOnlyList<IDomainEvent> DomainEvents => _events;
    protected void AddDomainEvent(IDomainEvent evt) => _events.Add(evt);
    public void ClearDomainEvents() => _events.Clear();
}
7How do you implement the Circuit Breaker pattern in .NET?

The Circuit Breaker prevents cascading failures β€” after N failures, it "opens" and fails fast instead of calling the failing dependency. After a timeout, it allows a test request ("half-open"). If successful, it "closes" again.

// Polly (Microsoft.Extensions.Resilience wraps Polly)
// .NET 8+ β€” use Microsoft.Extensions.Http.Resilience
builder.Services.AddHttpClient<PaymentService>()
    .AddResilienceHandler("payment", builder => {
        // Retry 3 times with exponential backoff before circuit breaker sees failure
        builder.AddRetry(new HttpRetryStrategyOptions {
            MaxRetryAttempts = 3,
            Delay             = TimeSpan.FromMilliseconds(500),
            BackoffType       = DelayBackoffType.Exponential,
            UseJitter         = true,
        });
        // Circuit breaker
        builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions {
            FailureRatio      = 0.5,              // open if 50% fail
            SamplingDuration  = TimeSpan.FromSeconds(30),
            MinimumThroughput = 10,               // need at least 10 calls to evaluate
            BreakDuration     = TimeSpan.FromSeconds(30), // stay open for 30s
            OnOpened = args => {
                logger.LogWarning("Circuit opened: {Reason}", args.Outcome.Exception?.Message);
                return ValueTask.CompletedTask;
            },
        });
        builder.AddTimeout(TimeSpan.FromSeconds(10));
    });

// Handle circuit breaker exception
try {
    var result = await paymentService.ProcessAsync(request, ct);
}
catch (BrokenCircuitException) {
    // Circuit is open β€” return fallback or 503
    return ServiceUnavailable("Payment service unavailable. Please try again later.");
}
8What is the Mediator pattern? How does it reduce coupling between components?

The Mediator pattern defines an object (the mediator) that encapsulates how a set of objects interact, preventing direct object references and reducing tight coupling. In .NET, MediatR implements this with commands, queries, and notifications.

// Without Mediator β€” controller directly depends on 5 services
class OrderController(
    IOrderService orderSvc,
    IInventoryService inventorySvc,
    IPaymentService paymentSvc,
    INotificationService notificationSvc,
    ILogger<OrderController> logger) { }

// With Mediator β€” controller depends only on IMediator
class OrderController(IMediator mediator) : ControllerBase {
    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderDto dto, CancellationToken ct) {
        var orderId = await mediator.Send(new CreateOrderCommand(dto), ct);
        return CreatedAtAction(nameof(Get), new { id = orderId }, null);
    }
}

// Notification β€” publish domain events to multiple handlers (fan-out)
record OrderConfirmedEvent(Guid OrderId, decimal Total) : INotification;

class SendConfirmationEmailHandler(IEmailService email)
    : INotificationHandler<OrderConfirmedEvent> {
    public Task Handle(OrderConfirmedEvent e, CancellationToken ct)
        => email.SendConfirmationAsync(e.OrderId, e.Total, ct);
}

class UpdateInventoryHandler(IInventoryService inventory)
    : INotificationHandler<OrderConfirmedEvent> {
    public Task Handle(OrderConfirmedEvent e, CancellationToken ct)
        => inventory.ReserveItemsAsync(e.OrderId, ct);
}

// Publish event β€” all handlers called (by default sequentially)
await mediator.Publish(new OrderConfirmedEvent(order.Id, order.Total), ct);

Testing

6 questions
1How do you write unit tests in C# with xUnit, NUnit, or MSTest? What are the key differences?
// xUnit β€” recommended for new .NET projects
public class OrderServiceTests {
    private readonly Mock<IOrderRepository> _repoMock = new();
    private readonly OrderService _sut; // system under test

    public OrderServiceTests() {
        _sut = new OrderService(_repoMock.Object);
    }

    [Fact]
    public async Task CreateOrder_WithValidData_ReturnsOrderId() {
        // Arrange
        var dto = new CreateOrderDto(CustomerId: Guid.NewGuid(), Items: [...]);
        var expectedId = Guid.NewGuid();
        _repoMock.Setup(r => r.AddAsync(It.IsAny<Order>(), default))
                 .ReturnsAsync(expectedId);

        // Act
        var result = await _sut.CreateOrderAsync(dto);

        // Assert
        result.Should().Be(expectedId); // FluentAssertions
        _repoMock.Verify(r => r.AddAsync(It.Is<Order>(o => o.CustomerId == dto.CustomerId), default),
                         Times.Once);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(-999)]
    public async Task CreateOrder_WithInvalidQuantity_ThrowsValidationException(int quantity) {
        var dto = new CreateOrderDto(CustomerId: Guid.NewGuid(),
            Items: [new OrderItemDto(ProductId: Guid.NewGuid(), Quantity: quantity)]);

        await Assert.ThrowsAsync<ValidationException>(() => _sut.CreateOrderAsync(dto));
    }
}

// Differences:
// xUnit:   [Fact] / [Theory] β€” new instance per test (isolation), no [SetUp]/[TearDown]
// NUnit:   [Test] / [TestCase] β€” one instance per fixture, [SetUp]/[TearDown]
// MSTest:  [TestMethod] / [DataTestMethod] β€” similar to NUnit
// xUnit is most popular in .NET ecosystem, recommended by Microsoft
2How do you mock dependencies in .NET tests? Compare Moq, NSubstitute, and FakeItEasy.
// Moq β€” most widely used
var mock = new Mock<IOrderService>();
mock.Setup(s => s.GetOrderAsync(It.IsAny<Guid>(), default))
    .ReturnsAsync(new Order { Id = orderId });
mock.Setup(s => s.CreateOrderAsync(It.Is<CreateOrderDto>(d => d.CustomerId != Guid.Empty), default))
    .ThrowsAsync(new ConflictException());

mock.Verify(s => s.GetOrderAsync(orderId, default), Times.Once);
mock.VerifyNoOtherCalls(); // assert no unexpected calls

// NSubstitute β€” more fluent, less verbose
var sub = Substitute.For<IOrderService>();
sub.GetOrderAsync(orderId, default).Returns(new Order { Id = orderId });
sub.GetOrderAsync(Arg.Any<Guid>(), default).Returns(x => Task.FromResult<Order?>(null));

await sub.Received(1).GetOrderAsync(orderId, default);
await sub.DidNotReceive().CreateOrderAsync(Arg.Any<CreateOrderDto>(), default);

// FakeItEasy β€” similar fluency to NSubstitute
var fake = A.Fake<IOrderService>();
A.CallTo(() => fake.GetOrderAsync(orderId, default)).Returns(new Order());

A.CallTo(() => fake.GetOrderAsync(orderId, default)).MustHaveHappenedOnceExactly();

// Comparison:
// Moq: most popular, powerful but verbose, null-safety issues (MockBehavior.Strict)
// NSubstitute: fluent API, less boilerplate, great for test readability
// FakeItEasy: similar to NSubstitute, very readable syntax
// Choose NSubstitute or FakeItEasy for new projects
3How do you write integration tests for ASP.NET Core APIs using WebApplicationFactory?
public class OrderApiTests(CustomWebAppFactory factory) : IClassFixture<CustomWebAppFactory> {
    private readonly HttpClient _client = factory.CreateClient();

    [Fact]
    public async Task PostOrder_WithValidData_Returns201() {
        // Arrange
        var dto = new CreateOrderDto { CustomerId = Guid.NewGuid(), Items = [...] };

        // Act
        var response = await _client.PostAsJsonAsync("/api/orders", dto);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        var order = await response.Content.ReadFromJsonAsync<OrderDto>();
        order.Should().NotBeNull();
        order!.CustomerId.Should().Be(dto.CustomerId);
    }

    [Fact]
    public async Task GetOrder_NotFound_Returns404() {
        var response = await _client.GetAsync($"/api/orders/{Guid.NewGuid()}");
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }
}

// Custom factory β€” configure test dependencies
class CustomWebAppFactory : WebApplicationFactory<Program> {
    protected override void ConfigureWebHost(IWebHostBuilder builder) {
        builder.ConfigureTestServices(services => {
            // Replace real DB with test DB (Testcontainers)
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(opts =>
                opts.UseNpgsql(PostgresContainer.ConnectionString));

            // Replace external services with fakes
            services.AddSingleton<IEmailService, FakeEmailService>();
            services.AddSingleton<IPaymentGateway, FakePaymentGateway>();
        });

        builder.UseEnvironment("Testing");
    }
}

// Testcontainers β€” spin up real Postgres in Docker for tests
public class PostgresFixture : IAsyncLifetime {
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .WithDatabase("testdb")
        .Build();

    public static string ConnectionString { get; private set; } = "";

    public async Task InitializeAsync() {
        await _postgres.StartAsync();
        ConnectionString = _postgres.GetConnectionString();
    }
    public Task DisposeAsync() => _postgres.DisposeAsync().AsTask();
}
4What is Bogus and what are Builder/Mother patterns for test data creation?
// Bogus β€” realistic fake data generation
var orderFaker = new Faker<Order>()
    .RuleFor(o => o.Id,          f => f.Random.Guid())
    .RuleFor(o => o.CustomerId,  f => f.Random.Guid())
    .RuleFor(o => o.Total,       f => f.Finance.Amount(10, 10000))
    .RuleFor(o => o.Status,      f => f.PickRandom<OrderStatus>())
    .RuleFor(o => o.CreatedAt,   f => f.Date.Recent(30))
    .RuleFor(o => o.CustomerName,f => f.Person.FullName);

var order    = orderFaker.Generate();
var orders   = orderFaker.Generate(50);
var pending  = orderFaker.RuleFor(o => o.Status, OrderStatus.Pending).Generate(10);

// Test Builder pattern β€” fluent, explicit, composable
class OrderBuilder {
    private Guid _customerId = Guid.NewGuid();
    private OrderStatus _status = OrderStatus.Draft;
    private List<OrderItem> _items = [];

    public OrderBuilder WithCustomer(Guid customerId) { _customerId = customerId; return this; }
    public OrderBuilder Confirmed() { _status = OrderStatus.Confirmed; return this; }
    public OrderBuilder WithItems(params OrderItem[] items) { _items = items.ToList(); return this; }
    public Order Build() => new() { CustomerId = _customerId, Status = _status, Items = _items };

    // Convenience methods
    public static OrderBuilder AConfirmedOrder() => new OrderBuilder().Confirmed().WithItems(
        new OrderItem { ProductId = Guid.NewGuid(), Quantity = 1, Price = 99.99m }
    );
}

// Usage in tests β€” clear intent
var order = new OrderBuilder()
    .WithCustomer(customerId)
    .Confirmed()
    .WithItems(new OrderItem { Quantity = 2, Price = 50 })
    .Build();

// Object Mother β€” static factory methods for common test objects
static class OrderMother {
    public static Order Draft() => new OrderBuilder().Build();
    public static Order Confirmed() => new OrderBuilder().Confirmed().WithItems(SomeItem()).Build();
    public static Order Shipped() => Confirmed() with { Status = OrderStatus.Shipped };
}
5What is mutation testing and how does it measure test quality better than code coverage?

Mutation testing automatically introduces small bugs (mutations) into the source code and checks whether the existing tests detect them. If a mutant "survives" (no test fails), it indicates the tests are insufficient. The mutation score = killed / total mutants.

This is a stronger quality signal than code coverage β€” a test can achieve 100% line coverage without actually asserting anything meaningful. Mutation testing verifies that tests would fail if the code had bugs.

// Stryker.NET β€” .NET mutation testing tool
// Install: dotnet tool install dotnet-stryker
// Run: dotnet stryker

// Common mutation types Stryker introduces:
// - Arithmetic: + β†’ -, * β†’ /
// - Conditional: > β†’ >=, == β†’ !=
// - Boolean literals: true β†’ false
// - Null checks: != null β†’ == null
// - Method calls: .Any() β†’ .All(), .First() β†’ .Last()
// - Boundary conditions: i < n β†’ i <= n

// Example: code under test
public bool IsEligibleForDiscount(Customer customer) {
    return customer.TotalOrders > 10 && customer.IsActive;
}

// Mutant 1: customer.TotalOrders > 10 β†’ customer.TotalOrders >= 10
// Mutant 2: customer.TotalOrders > 10 β†’ customer.TotalOrders < 10
// Mutant 3: && β†’ ||
// Mutant 4: customer.IsActive β†’ !customer.IsActive

// A good test must kill all 4 mutants:
[Fact]
public void IsEligibleForDiscount_BoundaryCondition_Matters() {
    var customer = new Customer { TotalOrders = 10, IsActive = true };
    sut.IsEligibleForDiscount(customer).Should().BeFalse(); // kills mutant 1
    new Customer { TotalOrders = 11, IsActive = true }
        .IsEligibleForDiscount().Should().BeTrue();         // kills mutant 2
}
6What is Snapshot testing and Approval testing in .NET?

Snapshot testing saves the output of a test on first run as a "snapshot" file. On subsequent runs, it compares the output to the snapshot β€” failing if they differ. Useful for complex objects, HTML output, JSON responses, and generated code.

Approval testing (Verify library) extends snapshot testing with a reviewer-approves workflow β€” the test generates "received" output, a human approves it by renaming to "approved," and future runs compare against approved output.

// Verify β€” approval testing library
[Fact]
public async Task GetOrder_ReturnsCorrectJson() {
    // Act
    var order = await orderService.GetOrderAsync(orderId);

    // Assert β€” Verify compares to approved file
    await Verify(order); // serialises to JSON and compares to *.verified.json
}

// Snapshot of complex objects
[Fact]
public async Task GenerateReport_MatchesSnapshot() {
    var report = await reportService.GenerateAsync(DateTime.Parse("2024-01-01"));
    await Verify(report)
        .ScrubMember<Report>(r => r.GeneratedAt) // ignore timestamp in comparison
        .UseDirectory("Snapshots");
}

// Useful for:
// - API contract testing β€” ensure response shape doesn't accidentally change
// - Report generation β€” verify output without hand-coding all assertions
// - Generated code β€” test code generators
// - Serialisation β€” verify JSON/XML output of custom serialisers

Performance

6 questions
1How do you profile a .NET application? What tools are available and what do you look for?

Tools:

  • BenchmarkDotNet β€” micro-benchmarking library. Accurate measurements with warmup, multiple runs, statistical analysis, memory allocation reporting. The standard for .NET benchmarking.
  • PerfView β€” Microsoft's free CPU/memory profiler. Captures ETW events. Deep analysis of GC, JIT, thread contention. Steep learning curve but very powerful.
  • dotnet-trace / dotnet-counters / dotnet-dump β€” cross-platform diagnostic tools. Available as global tools. Collect runtime events, monitor real-time metrics, capture heap dumps.
  • JetBrains dotMemory / dotTrace β€” commercial, excellent UI. dotTrace for CPU, dotMemory for heap analysis.
  • Visual Studio Diagnostic Tools β€” integrated profiler for CPU sampling and memory snapshots.
// BenchmarkDotNet β€” measure allocations and performance
[MemoryDiagnoser]
[RankColumn]
public class StringConcatBenchmark {
    private readonly string[] _parts = Enumerable.Range(0, 100).Select(i => i.ToString()).ToArray();

    [Benchmark(Baseline = true)]
    public string StringConcat() => string.Concat(_parts);

    [Benchmark]
    public string StringBuilder() {
        var sb = new System.Text.StringBuilder();
        foreach (var s in _parts) sb.Append(s);
        return sb.ToString();
    }

    [Benchmark]
    public string StringJoin() => string.Join("", _parts);
}
// Run: dotnet run -c Release
// Output: | Method | Mean | Allocated |

What to look for: high GC pressure (frequent Gen 0 collections, LOH allocations), thread contention (high lock wait time), memory leaks (growing old-gen heap), hot paths with unexpected allocations (boxing, closures, LINQ in tight loops), async state machine allocations.

2What are the most important performance improvements in .NET 8 and .NET 9?
  • Dynamic PGO (Profile-Guided Optimisation) β€” the JIT collects runtime profiling data during tier-0 execution and uses it to make better optimisation decisions in tier-1. Enables speculative devirtualisation, inlining based on actual call sites. Can yield 10–30% throughput improvement with no code changes.
  • AVX-512 SIMD vectorisation β€” .NET 8 enables SIMD on more CPUs (Intel Ice Lake+). Array operations, string processing, and numeric computations use wider vectors automatically.
  • Frozen Collections β€” FrozenDictionary and FrozenSet for lookup tables built once and read many times. Perfect hashing enables faster lookups than regular Dictionary.
  • Green threads / async I/O improvements β€” .NET 8 introduced experimental green thread support (user-mode scheduling). HTTP/2 and HTTP/3 performance significantly improved.
  • Exception handling improvements β€” stack overflow detection, faster catch/finally, reduced overhead for non-exceptional paths.
  • LINQ improvements β€” CountBy, AggregateBy, Index added; many operators optimised to avoid allocations.
  • System.Text.Json improvements β€” faster serialisation, JsonObject support, interface hierarchy support, required properties.
// .NET 9 β€” SearchValues (blazing fast multi-character search)
private static readonly SearchValues<char> InvalidChars
    = SearchValues.Create("<>\"/\\?*:");

bool ContainsInvalidChar(ReadOnlySpan<char> path)
    => path.IndexOfAny(InvalidChars) >= 0; // SIMD-accelerated
3What are string interning and string optimisations in .NET? When does string manipulation become a bottleneck?
// String is immutable β€” every "modification" allocates a new string
string s = "hello";
s = s.ToUpper();     // allocates a new string "HELLO"
s = s + " world";    // allocates "HELLO world"
s = s.Replace("L", ""); // allocates "HEO word"

// Use StringBuilder for many concatenations
var sb = new StringBuilder(1024); // pre-allocate capacity
for (int i = 0; i < 1000; i++) sb.Append(i).Append(',');
string result = sb.ToString(); // one final allocation

// Use Span<char> / interpolated string handlers for zero-alloc formatting
// System.Text.StringBuilder has AddHandler support in .NET 6+
Span<char> buffer = stackalloc char[128];
if (value.TryFormat(buffer, out int written, "N2"))
    DoSomethingWith(buffer[..written]); // no heap allocation

// String interning β€” intern stores strings in a pool
string a = string.Intern("hello");
string b = string.Intern("hello");
ReferenceEquals(a, b); // true β€” same reference from pool
// String literals are automatically interned
// Explicit interning for frequently-used strings reduces memory

// StringComparison β€” always specify for locale-correct behaviour
string.Equals(a, b, StringComparison.Ordinal);          // byte comparison (fastest)
string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // case-insensitive
string.Compare(a, b, StringComparison.CurrentCulture);   // locale-aware (sort order)

// Common bottlenecks:
// - String.Split in a hot path (allocates string[] + N strings)
// - LINQ on strings calling ToLower()/ToUpper() per comparison
// - string.Format vs string interpolation (both allocate, use params overloads)
// - Use Regex.IsMatch with [GeneratedRegex] (source gen, zero JIT overhead)
4What is the ObjectPool pattern and when should you use it in .NET?

Object pooling reuses instances of expensive-to-create objects (avoiding GC pressure from frequent allocation and collection).

// Microsoft.Extensions.ObjectPool
public class PooledStringBuilder {
    private readonly ObjectPool<StringBuilder> _pool;

    public PooledStringBuilder(ObjectPoolProvider provider) {
        _pool = provider.CreateStringBuilderPool();
    }

    public string FormatData(IEnumerable<string> items) {
        var sb = _pool.Get(); // rent from pool
        try {
            foreach (var item in items) sb.Append(item).Append(',');
            return sb.ToString();
        } finally {
            _pool.Return(sb); // return to pool (pool resets the StringBuilder)
        }
    }
}

// Custom pooled object
class MyPooledObject {
    public void Reset() { /* clear state before returning to pool */ }
}

class MyObjectPolicy : PooledObjectPolicy<MyPooledObject> {
    public override MyPooledObject Create() => new MyPooledObject();
    public override bool Return(MyPooledObject obj) {
        obj.Reset();
        return true; // return true to allow pooling, false to discard
    }
}

// Register in DI
builder.Services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
builder.Services.AddSingleton(sp => {
    var provider = sp.GetRequiredService<ObjectPoolProvider>();
    return provider.Create(new MyObjectPolicy());
});

// Use pooling for:
// - StringBuilder (built-in pool support)
// - HttpRequestMessage (reuse for outgoing requests)
// - Byte arrays > ArrayPool<T> for temporary buffers
// - Complex objects that are expensive to initialise
// - Regex match objects
5How does response compression and streaming improve ASP.NET Core API performance?
// Response compression β€” reduce bytes transferred
builder.Services.AddResponseCompression(options => {
    options.EnableForHttps = true; // enable for HTTPS (BREACH attack risk β€” evaluate for your app)
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
        ["application/json", "application/javascript"]);
});
builder.Services.Configure<BrotliCompressionProviderOptions>(opts =>
    opts.Level = CompressionLevel.Fastest);

app.UseResponseCompression(); // must be before UseStaticFiles, UseRouting

// Streaming response β€” send data as it's generated (no buffering)
app.MapGet("/stream", async (HttpContext ctx, CancellationToken ct) => {
    ctx.Response.ContentType = "text/event-stream";
    ctx.Response.Headers.Append("Cache-Control", "no-cache");
    ctx.Response.Headers.Append("X-Accel-Buffering", "no"); // disable nginx buffering

    await foreach (var item in GetItemsAsync(ct)) {
        await ctx.Response.WriteAsync($"data: {JsonSerializer.Serialize(item)}\n\n", ct);
        await ctx.Response.Body.FlushAsync(ct); // flush each chunk immediately
    }
});

// IAsyncEnumerable β€” automatic streaming in minimal APIs
app.MapGet("/products", (IProductService svc, CancellationToken ct)
    => svc.StreamProductsAsync(ct)); // returned IAsyncEnumerable is streamed as JSON array

// Output buffering β€” disable to reduce memory for large responses
builder.Services.AddControllers(opts => {
    opts.SuppressAsyncSuffixInActionNames = false;
    // Disable output buffering for streaming actions
});
// Or per-action: [DisableResponseBuffering]
6What is Kestrel and how do you tune it for high-throughput ASP.NET Core applications?

Kestrel is ASP.NET Core's built-in, cross-platform, high-performance web server. It handles HTTP/1.1, HTTP/2, HTTP/3 (QUIC), and WebSockets. It's faster than IIS for most scenarios.

// Kestrel configuration β€” tuning for high throughput
builder.WebHost.ConfigureKestrel(options => {
    // Connection limits
    options.Limits.MaxConcurrentConnections = 10_000;
    options.Limits.MaxConcurrentUpgradedConnections = 1_000; // WebSockets

    // Request limits
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
    options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
    options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);

    // HTTP/2 tuning
    options.Limits.Http2.MaxStreamsPerConnection = 100;
    options.Limits.Http2.InitialConnectionWindowSize = 131_072;  // 128 KB
    options.Limits.Http2.InitialStreamWindowSize = 98_304;       // 96 KB

    // Listen on specific addresses
    options.Listen(IPAddress.Any, 8080);
    options.Listen(IPAddress.Any, 8443, listenOptions => {
        listenOptions.UseHttps("/certs/cert.pfx");
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});

// Thread pool tuning β€” avoid thread starvation
ThreadPool.SetMinThreads(200, 200); // pre-warm thread pool
// Or in csproj: <ThreadPoolMinThreads>200</ThreadPoolMinThreads>

// Server GC β€” essential for throughput
// Set in runtimeconfig.json or csproj:
// <ServerGarbageCollection>true</ServerGarbageCollection>
// <GarbageCollectionAdapationMode>0</GarbageCollectionAdapationMode>

Microservices

6 questions
1What is gRPC in .NET? When do you prefer it over REST?

gRPC is a high-performance, contract-first RPC framework using HTTP/2 and Protocol Buffers (Protobuf) for binary serialisation. .NET has first-class support via the Grpc.AspNetCore package.

// orders.proto
syntax = "proto3";
option csharp_namespace = "OrderService.Grpc";

service Orders {
  rpc GetOrder (GetOrderRequest) returns (OrderResponse);
  rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse); // server streaming
  rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
}

message GetOrderRequest { string order_id = 1; }
message OrderResponse {
  string id = 1;
  string customer_id = 2;
  double total = 3;
  string status = 4;
}

// Server implementation
public class OrdersService(IOrderRepository repo) : Orders.OrdersBase {
    public override async Task<OrderResponse> GetOrder(
        GetOrderRequest request, ServerCallContext context) {
        var order = await repo.GetByIdAsync(Guid.Parse(request.OrderId), context.CancellationToken);
        return order == null
            ? throw new RpcException(new Status(StatusCode.NotFound, "Order not found"))
            : MapToResponse(order);
    }

    public override async Task ListOrders(
        ListOrdersRequest request,
        IServerStreamWriter<OrderResponse> responseStream,
        ServerCallContext context) {
        await foreach (var order in repo.StreamAsync(context.CancellationToken))
            await responseStream.WriteAsync(MapToResponse(order));
    }
}

// Register
builder.Services.AddGrpc();
app.MapGrpcService<OrdersService>();

gRPC vs REST:

  • Choose gRPC: internal service-to-service communication, streaming (chat, real-time dashboards), polyglot microservices needing strong contracts, high-throughput low-latency scenarios (3–10Γ— faster than REST with JSON).
  • Choose REST: public APIs (browser clients can't use gRPC directly), simpler tooling requirements, when JSON human-readability matters for debugging.
2What is MassTransit and how does it simplify message-based microservice communication?

MassTransit is an open-source message bus abstraction for .NET that works with RabbitMQ, Azure Service Bus, Amazon SQS, and Kafka. It provides: automatic consumer discovery, retry/error handling, sagas (distributed state machines), request-response, and built-in saga persistence.

// Registration
builder.Services.AddMassTransit(config => {
    config.AddConsumers(Assembly.GetExecutingAssembly()); // auto-discover
    config.AddSagas(Assembly.GetExecutingAssembly());

    config.UsingRabbitMq((ctx, cfg) => {
        cfg.Host("rabbitmq", h => {
            h.Username("guest");
            h.Password("guest");
        });
        cfg.ConfigureEndpoints(ctx); // auto-configure queues from consumers
    });
});

// Message contract
record OrderSubmittedEvent(Guid OrderId, Guid CustomerId, decimal Total);

// Consumer
class OrderSubmittedConsumer(IOrderProcessor processor, ILogger<OrderSubmittedConsumer> logger)
    : IConsumer<OrderSubmittedEvent> {

    public async Task Consume(ConsumeContext<OrderSubmittedEvent> context) {
        logger.LogInformation("Processing order {OrderId}", context.Message.OrderId);
        await processor.ProcessAsync(context.Message.OrderId, context.CancellationToken);
    }
}

// Publisher
class OrderService(IPublishEndpoint publisher) {
    public async Task CreateOrderAsync(CreateOrderDto dto, CancellationToken ct) {
        var order = Order.Create(dto);
        await SaveAsync(order);
        await publisher.Publish(new OrderSubmittedEvent(order.Id, dto.CustomerId, order.Total), ct);
    }
}
3What is distributed tracing and how do you implement it with OpenTelemetry in .NET?
// OpenTelemetry in .NET β€” vendor-neutral observability
builder.Services.AddOpenTelemetry()
    .ConfigureResource(res => res
        .AddService(serviceName: "OrderService", serviceVersion: "1.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()  // automatic: HTTP request tracing
        .AddHttpClientInstrumentation()  // automatic: outgoing HTTP calls
        .AddEntityFrameworkCoreInstrumentation() // automatic: EF Core queries
        .AddSource("OrderService")       // custom activity source
        .AddOtlpExporter(opts => opts.Endpoint = new Uri("http://otel-collector:4317")))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()     // GC, thread pool, JIT metrics
        .AddOtlpExporter());

// Custom traces
static readonly ActivitySource ActivitySource = new("OrderService");

async Task<Order> CreateOrderAsync(CreateOrderDto dto) {
    using var activity = ActivitySource.StartActivity("CreateOrder");
    activity?.SetTag("customer.id", dto.CustomerId.ToString());
    activity?.SetTag("order.item_count", dto.Items.Count);

    try {
        var order = await SaveOrderAsync(dto);
        activity?.SetTag("order.id", order.Id.ToString());
        activity?.SetStatus(ActivityStatusCode.Ok);
        return order;
    }
    catch (Exception ex) {
        activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
        activity?.RecordException(ex);
        throw;
    }
}
4How do you implement a health check system in ASP.NET Core for Kubernetes readiness and liveness probes?
// Registration
builder.Services.AddHealthChecks()
    // Built-in checks
    .AddNpgSql(connectionString, name: "postgres", tags: ["ready"])
    .AddRedis(redisConnectionString, name: "redis", tags: ["ready"])
    .AddUrlGroup(new Uri("https://external-api.com/health"), name: "external", tags: ["ready"])
    // Custom check
    .AddCheck<DiskSpaceHealthCheck>("disk", tags: ["ready"])
    .AddCheck("startup-complete", () => {
        return _startupComplete
            ? HealthCheckResult.Healthy()
            : HealthCheckResult.Unhealthy("Startup not complete");
    }, tags: ["live"]);

// Custom health check
class DiskSpaceHealthCheck : IHealthCheck {
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext ctx, CancellationToken ct) {
        var drive = DriveInfo.GetDrives().FirstOrDefault(d => d.Name == "/");
        if (drive == null) return Task.FromResult(HealthCheckResult.Unhealthy("Drive not found"));

        var freePercent = 100.0 * drive.AvailableFreeSpace / drive.TotalSize;
        return Task.FromResult(freePercent switch {
            > 20 => HealthCheckResult.Healthy($"Disk: {freePercent:F1}% free"),
            > 10 => HealthCheckResult.Degraded($"Disk low: {freePercent:F1}% free"),
            _    => HealthCheckResult.Unhealthy($"Disk critical: {freePercent:F1}% free"),
        });
    }
}

// Map endpoints β€” separate liveness and readiness
app.MapHealthChecks("/healthz/live", new HealthCheckOptions {
    Predicate = check => check.Tags.Contains("live") || !check.Tags.Any(),
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});

app.MapHealthChecks("/healthz/ready", new HealthCheckOptions {
    Predicate = check => check.Tags.Contains("ready"),
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});
5What is Saga pattern and how do you implement it with MassTransit for long-running distributed transactions?

A Saga is a sequence of local transactions coordinated through events/messages. Each step publishes an event that triggers the next step. If any step fails, compensating transactions roll back previous steps. Two implementations: choreography (services react to events) and orchestration (a central coordinator directs).

// Order fulfillment saga β€” orchestration with MassTransit StateMachine
public class OrderFulfillmentSaga : MassTransitStateMachine<OrderFulfillmentState> {
    public State Submitted   { get; private set; } = null!;
    public State PaymentPaid { get; private set; } = null!;
    public State Shipped     { get; private set; } = null!;

    public Event<OrderSubmitted> OrderSubmitted { get; private set; } = null!;
    public Event<PaymentCompleted> PaymentCompleted { get; private set; } = null!;
    public Event<PaymentFailed> PaymentFailed { get; private set; } = null!;
    public Event<ItemsShipped> ItemsShipped { get; private set; } = null!;

    public OrderFulfillmentSaga() {
        InstanceState(s => s.CurrentState);
        Event(() => OrderSubmitted,   e => e.CorrelateById(m => m.Message.OrderId));
        Event(() => PaymentCompleted, e => e.CorrelateById(m => m.Message.OrderId));
        Event(() => PaymentFailed,    e => e.CorrelateById(m => m.Message.OrderId));

        Initially(
            When(OrderSubmitted)
                .Then(ctx => ctx.Saga.CustomerId = ctx.Message.CustomerId)
                .Publish(ctx => new ProcessPaymentCommand(ctx.Saga.CorrelationId))
                .TransitionTo(Submitted));

        During(Submitted,
            When(PaymentCompleted)
                .Publish(ctx => new ShipItemsCommand(ctx.Saga.CorrelationId))
                .TransitionTo(PaymentPaid),
            When(PaymentFailed)
                .Publish(ctx => new CancelOrderCommand(ctx.Saga.CorrelationId))
                .Finalize());

        During(PaymentPaid,
            When(ItemsShipped).TransitionTo(Shipped).Finalize());
    }
}
6What is .NET Aspire and how does it simplify microservice development and observability?

.NET Aspire (stable in .NET 9) is an opinionated stack for building cloud-native, distributed applications. It provides: service orchestration, service discovery, built-in observability (OpenTelemetry pre-configured), and integrations for common infrastructure (Redis, Postgres, RabbitMQ, Azure services).

// AppHost β€” the orchestrator (runs in development, generates config for deployment)
var builder = DistributedApplication.CreateBuilder(args);

// Infrastructure
var redis    = builder.AddRedis("cache");
var postgres = builder.AddPostgres("db").AddDatabase("appdb");
var rabbit   = builder.AddRabbitMQ("messaging");

// Services β€” Aspire manages discovery and connection strings automatically
var apiService = builder.AddProject<Projects.Api>("api")
    .WithReference(redis)    // injects connection string as env var
    .WithReference(postgres)
    .WithReference(rabbit);

builder.AddProject<Projects.Worker>("worker")
    .WithReference(postgres)
    .WithReference(rabbit);

builder.Build().Run();

// Each service uses Aspire integration packages:
// Aspire.StackExchange.Redis β€” auto-configured IDistributedCache
// Aspire.Npgsql.EntityFrameworkCore.PostgreSQL β€” auto-configured DbContext
// Built-in Aspire Dashboard:
// - Distributed traces (Jaeger-compatible UI)
// - Structured logs across all services
// - Metrics dashboard

Aspire dramatically reduces the boilerplate for running multiple services locally, configuring their connections, and providing observability without external tooling. For deployment, it can generate Kubernetes manifests or Azure Container Apps configurations.

Modern .NET & AI

6 questions
1What is Semantic Kernel and how do you integrate LLMs into a .NET application?

Semantic Kernel is Microsoft's open-source SDK for integrating LLMs (OpenAI, Azure OpenAI, Ollama, Hugging Face) into .NET applications. It provides: plugin/function calling, memory (vector DB integration), prompt templates, agents, and process orchestration.

// Setup
builder.Services.AddKernel()
    .AddAzureOpenAIChatCompletion(
        deploymentName: "gpt-4o",
        endpoint: builder.Configuration["AzureOpenAI:Endpoint"]!,
        apiKey: builder.Configuration["AzureOpenAI:ApiKey"]!)
    .AddAzureOpenAITextEmbeddingGeneration("text-embedding-3-small", endpoint, apiKey);

// Chat completion
class OrderAssistant(Kernel kernel) {
    public async Task<string> AnswerQuestionAsync(string question, CancellationToken ct) {
        var result = await kernel.InvokePromptAsync<string>(
            "You are an order support assistant. Answer: {{$input}}",
            new KernelArguments { ["input"] = question },
            cancellationToken: ct);
        return result ?? "I couldn't answer that.";
    }
}

// Semantic function (native function as AI plugin)
class OrderPlugin(IOrderRepository repo) {
    [KernelFunction, Description("Get order status by order ID")]
    public async Task<string> GetOrderStatus(
        [Description("The order GUID")] string orderId,
        CancellationToken ct) {
        var order = await repo.GetByIdAsync(Guid.Parse(orderId), ct);
        return order == null ? "Order not found" : $"Order {orderId}: {order.Status}";
    }

    [KernelFunction, Description("List recent orders for a customer")]
    public async Task<IEnumerable<string>> GetRecentOrders(
        [Description("Customer ID")] string customerId, int count = 5, CancellationToken ct = default) {
        return (await repo.GetRecentAsync(Guid.Parse(customerId), count, ct))
            .Select(o => $"{o.Id}: {o.Total:C} - {o.Status}");
    }
}

// Register plugins and use function calling
kernel.ImportPluginFromType<OrderPlugin>();
var settings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };
var response = await kernel.InvokePromptAsync("What is the status of order 12345?",
    new(settings));
2How do you implement RAG (Retrieval-Augmented Generation) with vector databases in .NET?
// RAG pipeline with Semantic Kernel + Qdrant vector DB
builder.Services.AddKernel()
    .AddAzureOpenAIChatCompletion("gpt-4o", endpoint, apiKey)
    .AddAzureOpenAITextEmbeddingGeneration("text-embedding-3-small", endpoint, apiKey);

builder.Services.AddQdrantVectorStore("localhost"); // or AddAzureAISearchVectorStore

// Ingest documents β€” embed and store in vector DB
class DocumentIngestor(
    ITextEmbeddingGenerationService embeddingService,
    IVectorStore vectorStore) {

    public async Task IngestAsync(IEnumerable<Document> docs, CancellationToken ct) {
        var collection = vectorStore.GetCollection<ulong, DocumentRecord>("documents");
        await collection.CreateCollectionIfNotExistsAsync(ct);

        await Parallel.ForEachAsync(docs, ct, async (doc, token) => {
            // Chunk document into passages
            var chunks = ChunkDocument(doc.Content, maxTokens: 512, overlap: 50);

            foreach (var (chunk, idx) in chunks.Select((c, i) => (c, i))) {
                var embedding = await embeddingService.GenerateEmbeddingAsync(chunk, cancellationToken: token);
                await collection.UpsertAsync(new DocumentRecord {
                    Key     = (ulong)(doc.Id * 1000 + idx),
                    Content = chunk,
                    Source  = doc.Title,
                    Vector  = embedding,
                }, cancellationToken: token);
            }
        });
    }
}

// Query β€” retrieve relevant context, generate grounded answer
class RAGService(Kernel kernel, IVectorStore vectorStore) {
    public async Task<string> AnswerAsync(string question, CancellationToken ct) {
        // Step 1: embed the question
        var embeddingService = kernel.GetRequiredService<ITextEmbeddingGenerationService>();
        var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(question, cancellationToken: ct);

        // Step 2: retrieve similar documents
        var collection = vectorStore.GetCollection<ulong, DocumentRecord>("documents");
        var results = await collection.VectorizedSearchAsync(questionEmbedding,
            new() { Limit = 5 }, ct);

        // Step 3: build context from retrieved documents
        var context = string.Join("\n\n", await results.Results
            .Select(r => $"Source: {r.Record.Source}\n{r.Record.Content}")
            .ToListAsync(ct));

        // Step 4: generate grounded answer
        var prompt = $"""
            Answer the question using ONLY the provided context.
            If the answer isn't in the context, say "I don't have information about that."

            Context:
            {context}

            Question: {question}
            """;

        return (await kernel.InvokePromptAsync<string>(prompt, cancellationToken: ct))
               ?? "Unable to generate an answer.";
    }
}
3What is ML.NET and when do you use it over integrating with external LLM APIs?

ML.NET is Microsoft's open-source ML framework for .NET developers. It enables training and deploying ML models without leaving the .NET ecosystem β€” no Python required. Supports classification, regression, clustering, anomaly detection, recommendation, and image/text classification via model transfer.

// ML.NET β€” train and deploy a sentiment classifier
var mlContext = new MLContext(seed: 0);

// Load training data
IDataView trainingData = mlContext.Data.LoadFromTextFile<SentimentData>(
    "sentiment_train.csv", hasHeader: true, separatorChar: ',');

// Build pipeline
var pipeline = mlContext.Transforms.Text.FeaturizeText("Features", "Text")
    .Append(mlContext.BinaryClassification.Trainers.SdcaLogisticRegression());

// Train
ITransformer model = pipeline.Fit(trainingData);

// Evaluate
var testData = mlContext.Data.LoadFromTextFile<SentimentData>("sentiment_test.csv", hasHeader: true);
var metrics = mlContext.BinaryClassification.Evaluate(model.Transform(testData));
Console.WriteLine($"Accuracy: {metrics.Accuracy:P2}");

// Save model
mlContext.Model.Save(model, trainingData.Schema, "sentiment_model.zip");

// Prediction engine (thread-safe pool for ASP.NET Core)
var predEngine = mlContext.Model.CreatePredictionEngine<SentimentData, SentimentPrediction>(model);
var prediction = predEngine.Predict(new SentimentData { Text = "This product is amazing!" });
// prediction.PredictedLabel = true (positive), prediction.Score = 0.95

ML.NET vs External LLM API:

  • ML.NET: smaller, structured ML tasks (classification, regression, anomaly detection), data stays on-premise (compliance), low latency (local inference, no HTTP round-trip), no per-call API cost.
  • LLM API: complex reasoning, text generation, summarisation, code generation, few-shot tasks where no labelled training data exists, tasks requiring broad world knowledge.
4What is Microsoft.Extensions.AI and how does it provide a unified abstraction for AI services?

Microsoft.Extensions.AI (stable in .NET 9) provides a set of core abstractions for AI services β€” IChatClient, IEmbeddingGenerator<TInput, TEmbedding>, and ISpeechToTextClient β€” similar to how ILogger abstracts logging providers. Applications code against the interface; implementations (OpenAI, Azure OpenAI, Ollama, Anthropic) are injected.

// Application code β€” depends only on abstraction
class ContentAnalyser(IChatClient chatClient, IEmbeddingGenerator<string, Embedding<float>> embedder) {

    public async Task<string> SummariseAsync(string content, CancellationToken ct) {
        var response = await chatClient.CompleteAsync(
            [new ChatMessage(ChatRole.User, $"Summarise in 3 sentences:\n{content}")],
            cancellationToken: ct);
        return response.Message.Text ?? "";
    }

    public async Task<ReadOnlyMemory<float>> EmbedAsync(string text, CancellationToken ct) {
        var result = await embedder.GenerateAsync([text], cancellationToken: ct);
        return result[0].Vector;
    }
}

// Registration β€” swap providers without changing app code
// OpenAI
builder.Services.AddOpenAIClient(builder.Configuration["OpenAI:ApiKey"]!);
builder.Services.AddSingleton<IChatClient>(sp =>
    sp.GetRequiredService<OpenAIClient>()
      .AsChatClient("gpt-4o")
      .AsBuilder().UseLogging().UseFunctionInvocation().Build());

// Azure OpenAI (same interface)
builder.Services.AddSingleton<IChatClient>(sp =>
    new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey))
        .AsChatClient("gpt-4o-deployment"));

// Ollama (local models β€” same interface!)
builder.Services.AddSingleton<IChatClient>(_ =>
    new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.2"));
5How do you implement streaming LLM responses in an ASP.NET Core API and consume them from a client?
// Server β€” stream tokens via Server-Sent Events
app.MapPost("/api/chat/stream", async (
    ChatRequest request,
    IChatClient chatClient,
    HttpContext ctx,
    CancellationToken ct) => {

    ctx.Response.ContentType = "text/event-stream";
    ctx.Response.Headers.Append("Cache-Control", "no-cache");
    ctx.Response.Headers.Append("X-Accel-Buffering", "no");

    var messages = request.History.Select(m => new ChatMessage(
        m.Role == "user" ? ChatRole.User : ChatRole.Assistant, m.Content)).ToList();
    messages.Add(new ChatMessage(ChatRole.User, request.Message));

    await foreach (var update in chatClient.CompleteStreamingAsync(messages, cancellationToken: ct)) {
        if (!string.IsNullOrEmpty(update.Text)) {
            var data = JsonSerializer.Serialize(new { text = update.Text });
            await ctx.Response.WriteAsync($"data: {data}\n\n", ct);
            await ctx.Response.Body.FlushAsync(ct);
        }
    }

    await ctx.Response.WriteAsync("data: [DONE]\n\n", ct);
});

// Client β€” React/TypeScript consuming the SSE stream
async function streamChat(message: string, onToken: (text: string) => void) {
  const response = await fetch('/api/chat/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message, history: [] }),
  });

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const lines = decoder.decode(value).split('\n');
    for (const line of lines) {
      if (line.startsWith('data: ') && line !== 'data: [DONE]') {
        const { text } = JSON.parse(line.slice(6));
        onToken(text);
      }
    }
  }
}
6What are AI agents in .NET and how do you build a multi-step agentic workflow with Semantic Kernel?

An AI agent is an LLM-powered system that can reason, plan, use tools, and take actions iteratively to accomplish a goal. The agent decides which tools to call, interprets results, and continues reasoning until the task is complete.

// Multi-agent workflow with Semantic Kernel Agents (SK 1.x)
// Define specialised agents
var researchAgent = new ChatCompletionAgent {
    Name = "Researcher",
    Instructions = "Search and summarise relevant information. Be thorough and cite sources.",
    Kernel = kernel,
};

var writerAgent = new ChatCompletionAgent {
    Name = "Writer",
    Instructions = "Write polished content based on research. Use clear, engaging language.",
    Kernel = kernel,
};

var reviewerAgent = new ChatCompletionAgent {
    Name = "Reviewer",
    Instructions = "Review content for accuracy, clarity, and completeness. Provide specific feedback.",
    Kernel = kernel,
};

// Agent group chat β€” agents collaborate automatically
var groupChat = new AgentGroupChat(researchAgent, writerAgent, reviewerAgent) {
    ExecutionSettings = new() {
        TerminationStrategy = new ApprovalTerminationStrategy {
            Agents = [reviewerAgent],
            MaximumIterations = 10,
        },
        SelectionStrategy = new KernelFunctionSelectionStrategy(
            selectionFunction, kernel),
    },
};

// Run the agentic workflow
groupChat.AddChatMessage(new ChatMessageContent(AuthorRole.User,
    "Write a blog post about the benefits of async programming in .NET"));

await foreach (var message in groupChat.InvokeAsync()) {
    Console.WriteLine($"[{message.AuthorName}]: {message.Content}");
}

// Plugin for web search (tool the research agent uses)
[KernelFunction, Description("Search the web for information")]
async Task<string> SearchWebAsync([Description("Search query")] string query)
    => await webSearchService.SearchAsync(query);

Agentic workflows enable: complex multi-step research tasks, code generation and review pipelines, automated data analysis, customer support escalation with tool use. The key design principle: each agent has a clear, focused role; the orchestrator coordinates them.