💻 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.
}