Expert C# Interview Questions and Answers

๐Ÿ“‹ Table of Contents โ–พ
  1. Questions & Answers
  2. 📝 Knowledge Check

💻 Expert C# Interview Questions

This lesson targets senior engineers and architects. Topics include CLR internals, JIT compilation, the .NET GC, unsafe code and stackalloc, source generators, Roslyn analysers, lock-free programming, memory model, ref structs, NativeAOT, performance benchmarking, Clean Architecture, and production-grade patterns. These questions reveal whether you understand .NET deeply or just write C# in it.

Questions & Answers

01 How does the CLR (Common Language Runtime) work?

Internals The CLR is the .NET virtual machine โ€” it executes CIL (Common Intermediate Language) bytecode, manages memory, enforces type safety, and provides services like exception handling, thread management, and security.

CLR execution pipeline:

  • Compilation โ€” C# compiler (Roslyn) produces CIL bytecode + metadata stored in assemblies (.dll/.exe)
  • Loading โ€” CLR class loader reads assembly metadata, resolves dependencies, verifies CIL type safety
  • JIT Compilation โ€” Just-In-Time compiler converts CIL to native machine code on first call. Result is cached for subsequent calls.
  • Execution โ€” native code runs on the CPU; CLR manages the stack, heap, and thread lifecycle
  • GC โ€” Garbage Collector tracks heap-allocated objects, collects unreachable ones
// Inspect IL with: dotnet-ildasm, ILSpy, or sharplab.io
// Simple method compiled to IL
static int Add(int a, int b) => a + b;
// IL:
//   ldarg.0       (load a)
//   ldarg.1       (load b)
//   add           (add on stack)
//   ret           (return)

// Assembly = unit of deployment (contains IL + metadata + resources)
// AppDomain = isolation boundary within a process (single in .NET 5+)
// AssemblyLoadContext = extensible loader for plugins/isolation (.NET Core+)

// Tiered compilation (.NET Core 3+)
// Tier 0: unoptimised JIT -- fast startup
// Tier 1: re-JIT with optimisations after method is "hot"
// ReadyToRun (R2R): pre-JIT native code bundled with assembly for faster startup

// NativeAOT: compile entirely to native ahead of time (no JIT at runtime)
// dotnet publish -r linux-x64 -p:PublishAot=true

02 How does the .NET Garbage Collector work? What are the generations?

GC The .NET GC is a generational, mark-and-sweep compacting GC. It tracks which objects are reachable (live) from GC roots and collects everything else.

// Three generations -- based on object lifetime
// Gen 0: newly allocated objects -- collected most often (~milliseconds)
// Gen 1: survived Gen 0 -- short-lived objects that weren't collected
// Gen 2: long-lived objects -- collected least often (major GC, pause ~10-100ms)
// Large Object Heap (LOH): objects >= 85KB -- Gen 2, not compacted by default

// GC roots:
// - Local variables in active stack frames
// - Static variables
// - CPU registers holding object references
// - GC handles (pinned handles, weak references)

// Triggering a GC (normally automatic):
GC.Collect();                  // force full GC (avoid in production)
GC.Collect(0);                 // Gen 0 only
GC.GetTotalMemory(true);       // forces GC then returns bytes

// Check memory pressure
Console.WriteLine($"Gen 0: {GC.CollectionCount(0)}");
Console.WriteLine($"Gen 1: {GC.CollectionCount(1)}");
Console.WriteLine($"Gen 2: {GC.CollectionCount(2)}");
Console.WriteLine($"Heap:  {GC.GetTotalMemory(false):N0} bytes");

// Performance tips:
// Minimize allocations in hot paths (object pooling, ArrayPool, stackalloc)
// Avoid LOH fragmentation (reuse large arrays with ArrayPool)
// Use GC.AddMemoryPressure() when holding unmanaged memory
// Server GC vs Workstation GC: server GC uses one heap per CPU core

// ObjectPool -- reuse expensive objects
var pool = ObjectPool.Create<StringBuilder>();
var sb   = pool.Get();
sb.Clear(); sb.Append("work");
pool.Return(sb);  // back to pool

// ArrayPool -- reuse byte buffers without GC pressure
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
try   { await stream.ReadAsync(buffer); }
finally { ArrayPool<byte>.Shared.Return(buffer); }

03 What is unsafe code in C# and when do you use it?

Low Level The unsafe keyword enables direct pointer manipulation, bypassing the CLR’s type-safety checks. Required for some high-performance scenarios, P/Invoke interop, and systems programming.

// Enable in .csproj: <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

unsafe void PointerArithmetic()
{
    int[] arr = { 1, 2, 3, 4, 5 };

    // Pin the array in memory so GC won't move it
    fixed (int* ptr = arr)
    {
        for (int i = 0; i < arr.Length; i++)
            Console.Write(*(ptr + i) + " ");   // 1 2 3 4 5
    }
}

// stackalloc -- allocate on stack (no GC, very fast)
unsafe void StackAlloc()
{
    int* buffer = stackalloc int[128];   // 128 ints on the stack
    for (int i = 0; i < 128; i++) buffer[i] = i;
    // No cleanup needed -- stack frame is reclaimed automatically
}

// Safe alternative to stackalloc in modern C#
void SafeStackAlloc()
{
    Span<int> buffer = stackalloc int[128];   // no unsafe needed
    buffer.Fill(0);
}

// sizeof operator for value types
unsafe int GetSize() => sizeof(MyStruct);

// P/Invoke -- call native C library
[DllImport("libc.so.6")]
public static extern unsafe int memcpy(void* dest, void* src, nuint count);

// Unmanaged constraint -- use with generics in high-perf code
public static unsafe void ZeroMemory<T>(ref T value) where T : unmanaged
{
    fixed (T* ptr = &value)
        Unsafe.InitBlockUnaligned(ptr, 0, (uint)sizeof(T));
}

04 What are ref structs in C# and how do they differ from regular structs?

Low Level ref struct types are stack-only value types โ€” they cannot be promoted to the heap, boxed, used in arrays, or stored in fields of regular classes. This enables zero-allocation high-performance types like Span<T> and ReadOnlySpan<T>.

// ref struct -- stack only, cannot escape to heap
ref struct StackOnlyBuffer
{
    private Span<byte> _data;
    public StackOnlyBuffer(Span<byte> data) => _data = data;
    public int Length => _data.Length;
    public ref byte this[int i] => ref _data[i];
}

// Usage: must be in a local variable or method parameter
void Process()
{
    Span<byte> span = stackalloc byte[1024];
    var buffer = new StackOnlyBuffer(span);
    buffer[0] = 42;
    // buffer cannot be put in a class field, List, array, or async method
}

// Restrictions on ref struct:
// Cannot be: boxed, used as generic type argument (unless T : allows ref struct)
// Cannot be: stored in fields of class or non-ref struct
// Cannot be: captured by lambda, used across await/yield
// Cannot: implement interfaces (until C# 13 -- static interface members only)

// allows ref struct constraint (C# 13)
void GenericMethod<T>(T value) where T : allows ref struct { ... }

// Span<T> IS a ref struct -- that's why it cannot be stored in a field
// Use Memory<T> instead when heap storage is needed

05 What are C# source generators and how do you write one?

Roslyn Source generators (C# 9+ / Roslyn) run during compilation and emit additional C# source files into the compilation. They replace runtime reflection with compile-time code generation โ€” faster startup, AOT compatible, and type-safe.

// Incremental source generator (C# 10+ -- preferred over ISourceGenerator)
[Generator(LanguageNames.CSharp)]
public class AutoMapperGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Find all classes marked [AutoMap]
        var classDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (node, _) => node is ClassDeclarationSyntax c &&
                    c.AttributeLists.Count > 0,
                transform: static (ctx, _) => GetSemanticTarget(ctx))
            .Where(m => m is not null);

        context.RegisterSourceOutput(classDeclarations,
            static (spc, source) => Execute(spc, source!));
    }

    private static void Execute(SourceProductionContext context, ClassDeclarationSyntax cls)
    {
        var source = $$"""
            // Auto-generated
            public static partial class {{cls.Identifier}}Mapper
            {
                public static {{cls.Identifier}} Map({{cls.Identifier}}Dto dto)
                {
                    return new {{cls.Identifier}} { /* generated properties */ };
                }
            }
            """;
        context.AddSource($"{cls.Identifier}Mapper.g.cs", source);
    }
}

// Real-world source generators:
// - System.Text.Json source generation (replace runtime reflection with compiled serialisers)
// - LoggerMessage (zero-allocation logging)
// - Regex source gen ([GeneratedRegex])
// - EF Core model compilation
// - INotifyPropertyChanged implementations

06 What are Roslyn analysers and code fixes?

Roslyn Roslyn analysers inspect code at compile time and report diagnostics (warnings/errors). Code fixes provide automated refactorings to resolve the issues. Used by StyleCop, SonarAnalyzer, Microsoft.CodeAnalysis.NetAnalyzers.

// Simple analyser -- detect sync over async (deadlock risk)
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SyncOverAsyncAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id:             "MY001",
        title:          "Avoid .Result on Task",
        messageFormat:  "Calling .Result on Task can cause deadlocks; use await instead",
        category:       "Usage",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSyntaxNodeAction(AnalyseMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
    }

    private static void AnalyseMemberAccess(SyntaxNodeAnalysisContext context)
    {
        var node = (MemberAccessExpressionSyntax)context.Node;
        if (node.Name.Identifier.Text == "Result")
        {
            var type = context.SemanticModel.GetTypeInfo(node.Expression).Type;
            if (type?.Name == "Task")
                context.ReportDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
        }
    }
}

// Add to .csproj as NuGet package or via ProjectReference
// Analysers run in the IDE (red squiggles) and CI (build errors)

07 What is the .NET memory model and how does it affect multi-threaded code?

Threading The .NET memory model defines which memory operations are visible to which threads and in what order. Modern CPUs and compilers reorder instructions for performance โ€” fences/barriers prevent unsafe reorderings.

// volatile -- reads/writes are not reordered; always read from main memory
private volatile bool _running = true;

void StopLoop() => _running = false;  // visible to other threads immediately
void Loop() { while (_running) { /* work */ } }

// Thread.MemoryBarrier() -- full memory fence; no reads/writes cross it
void SetFlag()
{
    _data = 42;                  // write data first
    Thread.MemoryBarrier();      // full fence
    _ready = true;               // THEN set flag
}

// Interlocked.MemoryBarrier() / Volatile.Read / Volatile.Write
int value = Volatile.Read(ref _sharedValue);    // acquire semantics
Volatile.Write(ref _sharedValue, 42);           // release semantics

// Double-checked locking -- correct implementation
private MyObject? _instance;
private readonly object _lock = new();

public MyObject GetInstance()
{
    if (_instance == null)                   // first check (no lock)
    {
        lock (_lock)
        {
            if (_instance == null)           // second check (inside lock)
                _instance = new MyObject();
        }
    }
    return _instance;
}
// Works because lock has release semantics (writes published on exit)

// Simpler: Lazy<T> -- thread-safe lazy initialisation
private readonly Lazy<MyObject> _lazy = new(() => new MyObject());
public MyObject Instance => _lazy.Value;

08 What is lock-free programming in C#? How do CAS operations work?

Threading Lock-free programming uses atomic CPU instructions (Compare-and-Swap / CAS) instead of locks, eliminating thread blocking and reducing contention.

using System.Threading;

// Interlocked.CompareExchange -- CAS (Compare And Swap)
// "If current == expected, set to new value atomically; return previous value"
int expected = 0;
int newValue = 1;
int previous = Interlocked.CompareExchange(ref _value, newValue, expected);
bool succeeded = previous == expected;  // true if we actually swapped

// Lock-free stack (Treiber stack)
public class LockFreeStack<T>
{
    private class Node { public T Value; public Node? Next; }
    private Node? _head;

    public void Push(T value)
    {
        var node = new Node { Value = value };
        Node? current;
        do
        {
            current = _head;       // read current head
            node.Next = current;   // link new node to current head
        }
        while (Interlocked.CompareExchange(ref _head, node, current) != current);
        // retry if head changed between our read and CAS (another thread pushed)
    }

    public bool TryPop(out T? value)
    {
        Node? current, next;
        do
        {
            current = _head;
            if (current == null) { value = default; return false; }
            next = current.Next;
        }
        while (Interlocked.CompareExchange(ref _head, next, current) != current);
        value = current.Value;
        return true;
    }
}

// .NET concurrent collections (internally lock-free or fine-grained):
ConcurrentQueue<T>, ConcurrentStack<T>, ConcurrentBag<T>, ConcurrentDictionary<K,V>

09 How do you benchmark C# code with BenchmarkDotNet?

Performance BenchmarkDotNet is the standard .NET benchmarking library. It runs methods thousands of times with proper warmup, statistical analysis, and JIT considerations to produce reliable performance measurements.

// Install: dotnet add package BenchmarkDotNet

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]          // report allocations
[SimpleJob(RuntimeMoniker.Net80)]
public class StringBenchmarks
{
    private const int N = 10_000;
    private readonly string[] _words = Enumerable.Repeat("hello", N).ToArray();

    [Benchmark(Baseline = true)]
    public string StringConcat()
    {
        var result = string.Empty;
        foreach (var w in _words) result += w;
        return result;
    }

    [Benchmark]
    public string StringJoin() => string.Join("", _words);

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

    [Benchmark]
    public string SpanConcat()
    {
        var total = _words.Sum(w => w.Length);
        return string.Create(total, _words, (span, words) =>
        {
            int pos = 0;
            foreach (var w in words) { w.CopyTo(span[pos..]); pos += w.Length; }
        });
    }
}

// Run: dotnet run -c Release
// BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
// Output includes: Mean, StdDev, Ratio, Allocated bytes

10 What is NativeAOT in .NET and what constraints does it impose?

Performance NativeAOT (Ahead-of-Time compilation) compiles the entire .NET application to a self-contained native binary at publish time. No JIT, no CLR at startup โ€” millisecond cold starts and minimal memory footprint.

// Publish as NativeAOT
// dotnet publish -r linux-x64 -c Release -p:PublishAot=true

// Requirements and constraints:
// No runtime reflection by default (cannot call Type.GetMethods() dynamically)
// No Assembly.Load() or dynamic code generation (Emit, etc.)
// No unbound generics instantiated at runtime
// Limited support for dynamic proxies (Castle, Moq require workarounds)

// Trim-safe code -- no reflection
// System.Text.Json source generation: replace runtime reflection
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
internal partial class AppJsonContext : JsonSerializerContext { }

// Register in Program.cs
builder.Services.ConfigureHttpJsonOptions(opts =>
    opts.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));

// Use [DynamicallyAccessedMembers] to inform the linker
public static T Create<T>(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
    => (T)Activator.CreateInstance(type)!;

// rd.xml or ILLink.Substitutions.xml -- preserve types for reflection
// <RootDescriptor Include="rd.xml" /> in .csproj

// Benefits:
// ~5-15MB binary (vs 50-100MB with CLR)
// <50ms cold start (vs 100-500ms with JIT)
// Lower memory baseline
// Ideal for: containers, serverless, CLIs, embedded

11 What are C# 13 features?

Modern C# C# 13 ships with .NET 9 (November 2024). Key additions include lock object improvements, escape sequences, partial properties, and allows ref struct generics.

// 1. New lock type -- System.Threading.Lock (faster than object locking)
private readonly System.Threading.Lock _lock = new();
lock (_lock) { /* critical section */ }
// Under the hood: uses Lock.EnterScope() -- lighter than Monitor.Enter

// 2. allows ref struct constraint -- use ref structs as generic type arguments
void Process<T>(T value) where T : allows ref struct { ... }

IEnumerable<T> Iterate<T>() where T : allows ref struct { yield return default!; }

// 3. ref/unsafe in iterators and async methods -- with limitations
async Task<int> ComputeAsync()
{
    Span<int> local = stackalloc int[10];  // now allowed in async methods (limited scope)
    local[0] = 42;
    return await Task.FromResult(local[0]);
}

// 4. Partial properties and indexers
public partial class MyViewModel
{
    public partial string Name { get; set; }  // declaration
}
// In another file:
public partial class MyViewModel
{
    private string _name = "";
    public partial string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }
}

// 5. New escape sequences
char newEscape = '\e';   // escape character (0x1B / ESC) -- cleaner than '\x1B'

// 6. Implicit index access in object initialisers
var arr = new int[3] { 1, 2, 3 };
var _ = new SomeType { Values = { [^1] = 99 } };  // last element = 99

12 What are primary constructors in C# 12?

C# 12 Primary constructors (C# 12) allow constructor parameters to be declared directly on the class declaration. Parameters are in scope throughout the entire class body, useful for dependency injection and simple data classes.

// Before C# 12 -- boilerplate DI constructor
public class OrderService
{
    private readonly IOrderRepository _repo;
    private readonly IEmailService    _email;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IOrderRepository repo, IEmailService email, ILogger<OrderService> logger)
    {
        _repo   = repo;
        _email  = email;
        _logger = logger;
    }
}

// C# 12 -- primary constructor (much shorter)
public class OrderService(
    IOrderRepository repo,
    IEmailService    email,
    ILogger<OrderService> logger)
{
    // Parameters are in scope throughout the class
    public async Task<Order> CreateAsync(CreateOrderDto dto)
    {
        var order = await repo.CreateAsync(dto);   // use 'repo' directly
        await email.SendConfirmationAsync(order);
        logger.LogInformation("Order {Id} created", order.Id);
        return order;
    }
}

// Warning: primary constructor parameters are not automatically stored as fields
// If you need them after the constructor, capture them:
public class Counter(int initial)
{
    private int _count = initial;   // capture parameter into a field
    public void Increment() => _count++;
    public int Value => _count;
}

// Works for struct too
public struct Point(double x, double y)
{
    public double X { get; } = x;
    public double Y { get; } = y;
    public double Distance => Math.Sqrt(X*X + Y*Y);
}

13 What are Roslyn interceptors and when are they used?

Roslyn Interceptors (experimental, C# 12 / .NET 8) allow a source generator to replace a specific method call site with a generated method at compile time. Used by ASP.NET Core’s request delegate source generator for performance-critical paths.

// Interceptor -- replaces a specific call site with generated code
// The interceptor method must have the same signature as the intercepted method

// 1. Original method being intercepted
public static class Greeter
{
    public static string Greet(string name) => $"Hello, {name}!";
}

// 2. Generated interceptor (output of a source generator)
namespace MyApp.Generated;

[System.Runtime.CompilerServices.InterceptsLocation(
    filePath: "/MyApp/Program.cs",
    line: 10,                          // exact line of the call site
    character: 5)]                     // exact column
public static class GreeterInterceptor
{
    // Called INSTEAD of Greeter.Greet at the specified location
    public static string Greet(string name)
    {
        Console.WriteLine("[intercepted]");
        return $"Hi there, {name}!";
    }
}

// Real-world use: ASP.NET Core minimal API source generator
// app.MapGet("/", Handler) -- the source generator intercepts this call
// and replaces the runtime reflection-based delegate factory with
// compiled, strongly-typed code for NativeAOT compatibility

// Enable (experimental):
// <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);MyApp.Generated</InterceptorsPreviewNamespaces>

14 What is Clean Architecture for C# applications?

Architecture

// Solution structure (Clean Architecture + CQRS + MediatR)
MyApp.sln
  src/
    MyApp.Domain/                  // innermost -- no dependencies
      Entities/                    // Order, Product, Customer
      ValueObjects/                // Money, Address
      Aggregates/
      DomainEvents/                // OrderPlaced, PaymentReceived
      Interfaces/                  // IOrderRepository (defined here)
      Exceptions/                  // DomainException, InsufficientFundsException

    MyApp.Application/             // use cases -- depends only on Domain
      Commands/                    // CreateOrderCommand + Handler
      Queries/                     // GetOrderByIdQuery + Handler
      DTOs/                        // OrderDto, CreateOrderDto
      Interfaces/                  // IEmailService, ICacheService
      Behaviours/                  // MediatR pipeline: logging, validation, auth
      Mappings/                    // AutoMapper profiles

    MyApp.Infrastructure/          // adapters -- implements Application interfaces
      Persistence/                 // EF Core DbContext, Repositories
      Caching/                     // RedisCacheService
      Email/                       // SendGridEmailService
      ServiceCollectionExtensions  // DI registration

    MyApp.API/                     // outermost -- depends on Application
      Controllers/                 // thin -- just MediatR.Send()
      Middleware/
      Filters/
      Program.cs

  tests/
    MyApp.Domain.Tests/            // unit: pure functions, no I/O
    MyApp.Application.Tests/       // unit + integration
    MyApp.API.IntegrationTests/    // WebApplicationFactory end-to-end

// Dependency rule: inner layers never reference outer layers
// Domain knows nothing about EF Core, HTTP, or Redis
// Application defines interfaces; Infrastructure implements them

// MediatR pipeline behaviour
public class ValidationBehaviour<TRequest, TResponse>
    (IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(
        TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        var failures = validators
            .Select(v => v.Validate(request))
            .SelectMany(r => r.Errors)
            .Where(e => e != null)
            .ToList();

        if (failures.Any()) throw new ValidationException(failures);
        return await next();
    }
}

15 What is the Inline Array feature in C# 12?

C# 12 [InlineArray(N)] (C# 12) declares a fixed-size array inline within a struct โ€” no heap allocation, no bounds-checking overhead. Used in the runtime for fixed-size buffers without unsafe code.

// [InlineArray] -- fixed-size buffer without unsafe
[System.Runtime.CompilerServices.InlineArray(4)]
public struct Vector4
{
    private float _element;  // single field -- repeated N times internally
}

// Use like an array
var v = new Vector4();
v[0] = 1.0f; v[1] = 2.0f; v[2] = 3.0f; v[3] = 4.0f;

// Iterate
foreach (var component in v) Console.Write(component + " ");  // 1 2 3 4

// Span view -- zero allocation
Span<float> span = v;   // no copy, direct memory view

// Practical: fixed-size ring buffer
[InlineArray(16)]
public struct RingBuffer16<T>
{
    private T _element;
}

// Before inline arrays: required unsafe fixed keyword
public unsafe struct OldVector4 { public fixed float Elements[4]; }

// Generated IL: single field + [System.Runtime.CompilerServices.InlineArray] metadata
// The JIT and runtime understand this to mean "N repeated fields"
// No boxing, no bounds-check overhead, no heap allocation

16 How does the JIT compiler optimise C# code?

Internals

// JIT (RyuJIT) applies many optimisations automatically:

// 1. Inlining -- replace call with function body (eliminates call overhead)
[MethodImpl(MethodImplOptions.AggressiveInlining)]  // hint to always inline
static int Add(int a, int b) => a + b;

[MethodImpl(MethodImplOptions.NoInlining)]  // prevent inlining (for profiling)
void DoNotInlineMe() { ... }

// 2. Dead code elimination
static int Compute(bool debug)
{
    if (debug) { /* never called in prod */ }  // JIT removes in release
    return 42;
}

// 3. Loop unrolling -- JIT unrolls small fixed-iteration loops

// 4. Bounds check elimination -- JIT removes array bounds checks in simple loops
for (int i = 0; i < arr.Length; i++)
    sum += arr[i];   // no bounds check: JIT proves i is always valid

// 5. SIMD vectorisation -- use hardware vector instructions
using System.Numerics;
var v1 = new Vector<float>(data1);
var v2 = new Vector<float>(data2);
var sum = v1 + v2;  // single SIMD instruction, not a loop

// 6. Tiered compilation
// Tier 0: quick compile for startup speed (code with counters)
// Tier 1: full optimisation after N calls (default threshold ~30)

// 7. PGO (Profile Guided Optimisation) -- .NET 7+
// JIT uses runtime profile data to optimise hot paths
// dotnet publish -p:TieredPGO=true

// Check JIT output with: dotnet-disasm, JitDump, BenchmarkDotNet with DisassemblyDiagnoser
// [DisassemblyDiagnoser(printSource: true)]

17 What are common C# performance anti-patterns?

Performance

// 1. String concatenation in a loop
// BAD:
string result = "";
for (int i = 0; i < 10_000; i++) result += i;   // O(n^2) allocations
// GOOD:
var sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) sb.Append(i);

// 2. LINQ in hot paths (allocates IEnumerator, closures, lists)
// BAD (in tight loop):
var max = items.Max(x => x.Value);  // closure + enumeration
// GOOD:
int max = int.MinValue;
foreach (var item in items) if (item.Value > max) max = item.Value;

// 3. Async void -- exceptions are unobserved
// BAD:
async void DoWork() { await SomeTask(); }   // exceptions vanish
// GOOD:
async Task DoWork() { await SomeTask(); }

// 4. ConfigureAwait(false) missing in library code
// BAD (can deadlock in WinForms/ASP.NET Framework):
public async Task<string> ReadAsync() => await File.ReadAllTextAsync("f");
// GOOD for library code:
public async Task<string> ReadAsync() =>
    await File.ReadAllTextAsync("f").ConfigureAwait(false);

// 5. .Result or .Wait() on Task -- deadlocks!
// BAD:
var result = GetDataAsync().Result;    // DEADLOCK in sync context
// GOOD:
var result = await GetDataAsync();

// 6. Unnecessary allocations in hot paths
// BAD:
void Process(int[] data) { var span = new List<int>(data); ... }  // allocation
// GOOD:
void Process(ReadOnlySpan<int> data) { ... }  // no allocation, stack-friendly

// 7. Large objects on LOH -- use ArrayPool
// BAD:
byte[] buffer = new byte[1_000_000];   // LOH, not compacted
// GOOD:
var buffer = ArrayPool<byte>.Shared.Rent(1_000_000);
try { ... } finally { ArrayPool<byte>.Shared.Return(buffer); }

18 What is CQRS with MediatR in C#?

Architecture CQRS (Command Query Responsibility Segregation) separates reads (Queries) from writes (Commands). MediatR is an in-process mediator that dispatches requests to their handlers, keeping controllers thin.

// Install: dotnet add package MediatR

// Command -- write operation
public record CreateOrderCommand(int CustomerId, List<OrderItemDto> Items)
    : IRequest<OrderDto>;

// Command handler
public class CreateOrderHandler(AppDbContext db, IMapper mapper, IEmailService email)
    : IRequestHandler<CreateOrderCommand, OrderDto>
{
    public async Task<OrderDto> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = new Order { CustomerId = cmd.CustomerId };
        order.Items.AddRange(cmd.Items.Select(i => new OrderItem(i.ProductId, i.Qty)));
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);
        await email.SendConfirmationAsync(order);
        return mapper.Map<OrderDto>(order);
    }
}

// Query -- read operation
public record GetOrderByIdQuery(int Id) : IRequest<OrderDto?>;

public class GetOrderByIdHandler(AppDbContext db, IMapper mapper)
    : IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
    public async Task<OrderDto?> Handle(GetOrderByIdQuery q, CancellationToken ct)
    {
        var order = await db.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == q.Id, ct);
        return order is null ? null : mapper.Map<OrderDto>(order);
    }
}

// Register
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());

// Thin controller
[ApiController, Route("api/orders")]
public class OrdersController(IMediator mediator) : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<ActionResult<OrderDto>> GetById(int id)
        => await mediator.Send(new GetOrderByIdQuery(id)) is { } dto ? Ok(dto) : NotFound();

    [HttpPost]
    public async Task<ActionResult<OrderDto>> Create(CreateOrderCommand cmd)
    {
        var dto = await mediator.Send(cmd);
        return CreatedAtAction(nameof(GetById), new { id = dto.Id }, dto);
    }
}

19 How do you implement domain events in C# with EF Core?

Architecture

// Domain event marker interface
public interface IDomainEvent : INotification { }

// Event raised by the domain entity
public record OrderPlacedEvent(int OrderId, int CustomerId, decimal Total) : IDomainEvent;

// Domain entity raises events
public class Order
{
    private readonly List<IDomainEvent> _events = [];
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _events.AsReadOnly();

    public void Place(Customer customer)
    {
        Status = OrderStatus.Pending;
        _events.Add(new OrderPlacedEvent(Id, customer.Id, Total));
    }
    public void ClearDomainEvents() => _events.Clear();
}

// Event handler
public class SendConfirmationOnOrderPlaced(IEmailService email)
    : INotificationHandler<OrderPlacedEvent>
{
    public async Task Handle(OrderPlacedEvent e, CancellationToken ct)
        => await email.SendConfirmationAsync(e.CustomerId, e.OrderId);
}

// Dispatch events after SaveChanges via a MediatR pipeline behaviour
public class DomainEventDispatcher<TReq, TRes>(AppDbContext db, IPublisher publisher)
    : IPipelineBehavior<TReq, TRes> where TReq : notnull
{
    public async Task<TRes> Handle(TReq req, RequestHandlerDelegate<TRes> next, CancellationToken ct)
    {
        var response = await next();   // execute handler + SaveChanges

        var entities = db.ChangeTracker.Entries<Order>()
            .Select(e => e.Entity)
            .Where(o => o.DomainEvents.Any())
            .ToList();

        foreach (var entity in entities)
        {
            var events = entity.DomainEvents.ToList();
            entity.ClearDomainEvents();
            foreach (var evt in events) await publisher.Publish(evt, ct);
        }
        return response;
    }
}

20 What are the common C# concurrency patterns for high-throughput services?

Architecture

// 1. Semaphore-limited parallel processing
async Task ProcessWithLimitAsync<T>(IEnumerable<T> items, Func<T, Task> handler, int maxConcurrency = 10)
{
    var semaphore = new SemaphoreSlim(maxConcurrency);
    var tasks = items.Select(async item =>
    {
        await semaphore.WaitAsync();
        try   { await handler(item); }
        finally { semaphore.Release(); }
    });
    await Task.WhenAll(tasks);
}

// 2. Producer-consumer with Channel
async Task RunPipelineAsync(IEnumerable<string> urls, CancellationToken ct)
{
    var channel = Channel.CreateBounded<string>(100);

    var producer = Task.Run(async () => {
        foreach (var url in urls) await channel.Writer.WriteAsync(url, ct);
        channel.Writer.Complete();
    }, ct);

    var consumers = Enumerable.Range(0, 4).Select(_ => Task.Run(async () => {
        await foreach (var url in channel.Reader.ReadAllAsync(ct))
            await ProcessUrlAsync(url);
    }, ct));

    await Task.WhenAll([producer, ..consumers]);
}

// 3. Cache-aside pattern with async double-check
private readonly SemaphoreSlim _cacheLock = new(1, 1);

public async Task<Product?> GetProductAsync(int id, CancellationToken ct)
{
    if (_cache.TryGetValue(id, out var cached)) return cached;

    await _cacheLock.WaitAsync(ct);
    try
    {
        if (_cache.TryGetValue(id, out cached)) return cached; // double-check
        var product = await _db.Products.FindAsync(id, ct);
        if (product != null) _cache.Set(id, product, TimeSpan.FromMinutes(5));
        return product;
    }
    finally { _cacheLock.Release(); }
}

21 What are string.Create and interpolated string handlers in C#?

Performance

// string.Create -- build a string directly into allocated memory, zero intermediate copies
string FormatId(int id)
{
    return string.Create(10, id, static (span, state) =>
    {
        "ID:".CopyTo(span);
        state.TryFormat(span[3..], out _, "D7");  // write id into span[3..10]
    });
}

// More complex example -- format without intermediate strings
public static string FormatCurrency(decimal amount, string currency)
{
    var totalLen = currency.Length + 1 + 20;  // estimate
    return string.Create(totalLen, (amount, currency), static (span, state) =>
    {
        var (amt, cur) = state;
        cur.CopyTo(span);
        span[cur.Length] = ' ';
        amt.TryFormat(span[(cur.Length+1)..], out var written, "F2");
        // span is exact -- no allocation
    });
}

// Interpolated string handlers (C# 10+) -- allow libraries to customise $ interpolation
// Used by: ILogger (avoids allocation if log level not enabled)
_logger.LogInformation($"Order {orderId} created for {customerName}");
// The compiler calls LoggerMessageInterpolatedStringHandler instead of creating a string
// If Debug logging is disabled, the string is NEVER built -- zero allocation

// Custom interpolated string handler
[InterpolatedStringHandler]
public ref struct LogInterpolatedStringHandler
{
    private DefaultInterpolatedStringHandler _inner;
    public LogInterpolatedStringHandler(int literalLen, int formattedCount,
                                        ILogger logger, LogLevel level,
                                        out bool shouldAppend)
    {
        shouldAppend = logger.IsEnabled(level);  // skip building if not enabled
        _inner = shouldAppend
            ? new DefaultInterpolatedStringHandler(literalLen, formattedCount)
            : default;
    }
    // AppendLiteral, AppendFormatted, ToString() etc.
}

📝 Knowledge Check

🧠 Quiz Question 1 of 5

What are the three generations in the .NET GC and what determines when each is collected?





🧠 Quiz Question 2 of 5

What is the key constraint of ref struct types and why does it exist?





🧠 Quiz Question 3 of 5

What does a C# source generator do and why is it beneficial for NativeAOT?





🧠 Quiz Question 4 of 5

What does tiered compilation in .NET do and how does it improve both startup and throughput?





🧠 Quiz Question 5 of 5

Why is calling .Result or .Wait() on a Task dangerous in ASP.NET or UI contexts?





Tip: Expert C# interviews reward depth over syntax recall. For the GC, explain generational collection and the LOH before discussing ArrayPool. For async/await, describe the state machine and synchronisation context before discussing deadlocks. For source generators, explain why runtime reflection is incompatible with NativeAOT before describing the solution. Context and tradeoffs — not just what but why — separate expert answers from good ones.