💻 Advanced C# Interview Questions
This lesson targets mid-to-senior C# roles. Topics include async/await internals, extension methods, expression trees, dependency injection, Span<T>, tuples, IEnumerable vs IQueryable, attributes, covariance, IAsyncEnumerable, channels, threading, and the Task Parallel Library. These questions separate C# developers from those who architect with it.
Questions & Answers
01 How does async/await work in C#? What is the state machine? ►
Async async/await transforms an asynchronous method into a state machine at compile time. Each await is a suspension point โ the method yields its thread back to the caller and resumes on a continuation when the awaited operation completes.
// Simple async method
public async Task<string> FetchUserAsync(int userId)
{
var response = await _httpClient.GetAsync($"/users/{userId}"); // suspension point
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
// Parallel async calls -- all start simultaneously
public async Task<DashboardData> GetDashboardAsync()
{
var ordersTask = GetOrdersAsync(); // start all three
var productsTask = GetProductsAsync();
var usersTask = GetUsersAsync();
await Task.WhenAll(ordersTask, productsTask, usersTask); // wait for all
return new DashboardData
{
Orders = await ordersTask,
Products = await productsTask,
Users = await usersTask
};
}
// ConfigureAwait(false) -- don't capture synchronisation context
// Important in library code to avoid deadlocks
public async Task<string> ReadFileAsync(string path)
=> await File.ReadAllTextAsync(path).ConfigureAwait(false);
// Avoid async void (except event handlers) -- exceptions can't be caught
// BAD: async void DoWork() { await SomeTask(); }
// GOOD: async Task DoWork() { await SomeTask(); }
// CancellationToken -- cooperative cancellation
public async Task ProcessAsync(CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
ct.ThrowIfCancellationRequested(); // check and throw if cancelled
await DoItemAsync(i, ct);
}
}
02 What are extension methods in C#? ►
Syntax Extension methods add new methods to existing types without modifying or subclassing them. They are static methods in static classes where the first parameter uses the this keyword.
// Define extension methods in a static class
public static class StringExtensions
{
// 'this string' makes it callable on string instances
public static bool IsNullOrWhiteSpace(this string? s)
=> string.IsNullOrWhiteSpace(s);
public static string Truncate(this string s, int maxLength, string suffix = "...")
{
if (s.Length <= maxLength) return s;
return s[..(maxLength - suffix.Length)] + suffix;
}
public static string ToTitleCase(this string s)
=> System.Globalization.CultureInfo.CurrentCulture
.TextInfo.ToTitleCase(s.ToLower());
}
// Use like instance methods
"hello world".ToTitleCase() // "Hello World"
"A very long title...".Truncate(15) // "A very long ..."
((string?)null).IsNullOrWhiteSpace() // true
// Extend IEnumerable<T>
public static class EnumerableExtensions
{
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : class
=> source.Where(x => x is not null)!;
public static IEnumerable<IEnumerable<T>> Batch<T>(
this IEnumerable<T> source, int batchSize)
=> source.Select((x, i) => (x, i))
.GroupBy(t => t.i / batchSize, t => t.x);
}
// All LINQ methods are extension methods on IEnumerable<T>
// That's why they appear on all collections without modifying List, Array, etc.
03 What are lambda expressions and expression trees in C#? ►
Expressions
// Lambda expression -- anonymous function
Func<int, int> square = x => x * x;
Func<int,int,int> add = (a, b) => a + b;
Action<string> print = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;
// Multi-statement lambda
Func<int, string> classify = n =>
{
if (n < 0) return "negative";
if (n == 0) return "zero";
return "positive";
};
// Expression tree -- represents code as data (AST)
// Used by LINQ providers (Entity Framework, etc.) to translate to SQL
Expression<Func<int, int>> exprTree = x => x * x;
// Note: Expression<...> not Func<...> -- stores the tree, not compiled code
var body = (BinaryExpression)exprTree.Body;
var left = (ParameterExpression)body.Left;
Console.WriteLine($"{left.Name} {body.NodeType} {left.Name}"); // "x Multiply x"
// Entity Framework uses expression trees:
// This lambda is NOT compiled to IL -- it's converted to SQL
var query = dbContext.Products.Where(p => p.Price > 100);
// EF reads the expression tree and generates: WHERE Price > 100
// Compile an expression tree to a delegate
var compiled = exprTree.Compile();
compiled(5); // 25
// Build expression trees dynamically (for dynamic LINQ)
var param = Expression.Parameter(typeof(Product), "p");
var prop = Expression.Property(param, "Price");
var val = Expression.Constant(100m);
var gt = Expression.GreaterThan(prop, val);
var lambda = Expression.Lambda<Func<Product, bool>>(gt, param);
// lambda = p => p.Price > 100
04 What is dependency injection in C# and how does it work in .NET? ►
DI Dependency Injection (DI) is a design pattern where a class receives its dependencies rather than creating them. The built-in .NET DI container resolves and manages lifetimes automatically.
// Three lifetimes
services.AddTransient<IEmailService, EmailService>(); // new instance per request
services.AddScoped<IOrderService, OrderService>(); // one per HTTP request
services.AddSingleton<ICacheService, RedisCacheService>(); // one for app lifetime
// Constructor injection (preferred)
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;
}
public async Task<Order> CreateAsync(CreateOrderDto dto)
{
var order = await _repo.CreateAsync(dto);
await _email.SendConfirmationAsync(order);
_logger.LogInformation("Order {Id} created", order.Id);
return order;
}
}
// Register with factory
services.AddScoped<IDbContext>(sp =>
new AppDbContext(sp.GetRequiredService<DbContextOptions<AppDbContext>>()));
// Keyed services (C# 8 / .NET 8+)
services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");
// Inject by key
public class Checkout([FromKeyedServices("stripe")] IPaymentProcessor payment) { }
05 What are Span<T> and Memory<T> in C#? ►
Performance Span<T> is a stack-only, contiguous view of memory that enables zero-allocation slicing of arrays, strings, and unmanaged memory. Memory<T> is the heap-safe equivalent for async code.
// Span<T> -- zero-copy slicing
int[] array = { 1, 2, 3, 4, 5, 6, 7, 8 };
Span<int> span = array.AsSpan();
Span<int> slice = span[2..5]; // { 3, 4, 5 } -- NO allocation!
slice[0] = 99;
Console.WriteLine(array[2]); // 99 -- span is a VIEW, not a copy
// stackalloc -- allocate on stack (no GC overhead)
Span<byte> buffer = stackalloc byte[256];
// String parsing without allocation
ReadOnlySpan<char> text = "2026-04-22".AsSpan();
int year = int.Parse(text[0..4]); // no substring allocation
int month = int.Parse(text[5..7]);
int day = int.Parse(text[8..10]);
// MemoryMarshal -- low-level reinterpretation
Span<byte> bytes = stackalloc byte[4] { 0x01, 0x00, 0x00, 0x00 };
Span<int> ints = MemoryMarshal.Cast<byte, int>(bytes);
Console.WriteLine(ints[0]); // 1
// Memory<T> -- for async methods (Span is stack-only, can't cross await)
public async Task ProcessAsync(Memory<byte> buffer)
{
await stream.ReadAsync(buffer); // Memory can be used across awaits
var span = buffer.Span; // get Span within a sync context
}
// When to use Span:
// High-performance parsing, binary serialisation, zero-copy I/O, hot paths
06 What is the difference between IEnumerable<T> and IQueryable<T>? ►
LINQ
// IEnumerable<T> -- in-memory LINQ, runs on the client
// All filtering/sorting happens in C# after loading data
IEnumerable<Product> productsInMemory = _context.Products.ToList(); // loads ALL
var cheap = productsInMemory.Where(p => p.Price < 10); // filter in C#
// IQueryable<T> -- LINQ provider, expression tree translated (e.g., to SQL)
// Filtering happens in the DATABASE -- only matching rows are transferred
IQueryable<Product> query = _context.Products; // not yet executed
query = query.Where(p => p.Price < 10); // builds expression tree
var result = query.ToList(); // generates SQL + executes
// Generated SQL:
// SELECT * FROM Products WHERE Price < 10
// vs IEnumerable: SELECT * FROM Products (then filter in memory)
// Practical difference
public IEnumerable<Product> GetCheap()
=> _context.Products // IQueryable
.Where(p => p.Price < 10) // translated to SQL WHERE
.OrderBy(p => p.Name) // translated to ORDER BY
.Take(20); // translated to TOP 20 / LIMIT 20
// Returns 20 rows from DB
// ALWAYS use IQueryable for database queries
// Use AsEnumerable() when you need client-side evaluation (e.g., C# functions not in SQL)
var result2 = _context.Products
.Where(p => p.Price < 10) // in DB
.AsEnumerable() // switch to in-memory
.Where(p => MyComplexCSharpFunc(p)) // in-memory
07 What are attributes in C# and how does reflection work? ►
Reflection Attributes add declarative metadata to types and members. Reflection reads that metadata at runtime. Used by serialisers, ORMs, test frameworks, DI containers, and validators.
// Custom attribute
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class RangeAttribute : Attribute
{
public double Min { get; }
public double Max { get; }
public RangeAttribute(double min, double max) { Min = min; Max = max; }
}
// Apply attribute
public class Product
{
[Range(0, 100_000)]
public decimal Price { get; set; }
[Required, StringLength(200, MinimumLength = 2)]
public string Name { get; set; } = "";
}
// Read attributes via reflection
var props = typeof(Product).GetProperties();
foreach (var prop in props)
{
var range = prop.GetCustomAttribute<RangeAttribute>();
if (range != null)
Console.WriteLine($"{prop.Name}: [{range.Min}, {range.Max}]");
}
// Reflection -- inspect and invoke at runtime
Type type = typeof(Product);
type.Name // "Product"
type.GetProperties() // PropertyInfo[]
type.GetMethods() // MethodInfo[]
// Create instance via reflection
var product = Activator.CreateInstance<Product>();
var prop2 = type.GetProperty("Name")!;
prop2.SetValue(product, "Widget");
prop2.GetValue(product); // "Widget"
// Performance: reflection is slow (~100x slower than direct access)
// For hot paths: cache the MethodInfo/PropertyInfo or use compiled delegates
08 What are tuples in C# and when do you use them? ►
Syntax
// ValueTuple (C# 7+) -- lightweight, named, stack-allocated
var person = (Name: "Alice", Age: 30);
person.Name // "Alice"
person.Age // 30
// Deconstruction
var (name, age) = person; // or: (string name, int age) = person;
Console.WriteLine(name); // "Alice"
// Return multiple values from a method
public (bool Success, string Message, User? User) TryLogin(string email, string pass)
{
var user = _userRepo.Find(email);
if (user == null) return (false, "User not found", null);
if (!VerifyPassword(pass, user.Hash)) return (false, "Wrong password", null);
return (true, "OK", user);
}
var (ok, msg, user) = TryLogin("alice@example.com", "pass123");
if (!ok) Console.WriteLine(msg);
// Pattern matching with tuples
string direction = (dx, dy) switch
{
(0, 1) => "up",
(0, -1) => "down",
(1, 0) => "right",
(-1, 0) => "left",
_ => "diagonal"
};
// When to use tuples vs records:
// Tuple: simple one-off return values, short-lived, no methods needed
// Record: named, reusable, appears in public API, needs ToString/equality
09 What are covariance and contravariance in C#? ►
Type System
// Covariance (out T) -- can use more derived type than specified
// "You can treat a producer of derived as a producer of base"
IEnumerable<Dog> dogs = new List<Dog> { new Dog() };
IEnumerable<Animal> animals = dogs; // OK because IEnumerable is covariant (out T)
// IEnumerable<out T> -- T can only appear in output positions
// So: foreach (Animal a in dogs) -- this is safe, Dog IS-A Animal
// Contravariance (in T) -- can use more general type
// "You can treat a consumer of base as a consumer of derived"
Action<Animal> feedAnimal = a => a.Feed();
Action<Dog> feedDog = feedAnimal; // OK because Action is contravariant (in T)
// feedDog can accept a Dog, and feedAnimal can handle any Animal (including Dog)
// Define covariant interface
public interface IProducer<out T> // T in output only
{
T Produce();
}
// Define contravariant interface
public interface IConsumer<in T> // T in input only
{
void Consume(T item);
}
// Arrays in C# are covariant but NOT type-safe at runtime
Animal[] animalArr = new Dog[5]; // compiles (array covariance)
animalArr[0] = new Cat(); // ArrayTypeMismatchException at RUNTIME!
// This is a design mistake -- use IReadOnlyList<T> (covariant) instead
// Func is covariant on return, contravariant on parameters
Func<Animal> animalFactory = () => new Dog();
Func<Dog> dogFactory = animalFactory; // INVALID (Animal is not Dog)
// But Func is covariant on TResult (out TResult)
10 What are custom iterators and the yield return pattern? ►
Iterators yield return creates a state machine that lazily produces values on demand. The compiler generates the full IEnumerable<T>/IEnumerator<T> implementation automatically.
// Simple iterator
public IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
yield return a; // pause, return a, resume next iteration
(a, b) = (b, a + b);
}
}
foreach (var n in Fibonacci().Take(10))
Console.Write($"{n} "); // 0 1 1 2 3 5 8 13 21 34
// Infinite range with early termination
public IEnumerable<int> Range(int start, int step = 1)
{
while (true) { yield return start; start += step; }
}
Range(0, 2).TakeWhile(n => n < 10).ToList(); // [0,2,4,6,8]
// Iterator with yield break
public IEnumerable<string> ReadLines(string path)
{
using var reader = new StreamReader(path);
string? line;
while ((line = reader.ReadLine()) is not null)
{
if (line.StartsWith("#")) yield break; // stop iteration
yield return line;
}
}
// Async iterator (C# 8+) -- IAsyncEnumerable<T>
public async IAsyncEnumerable<string> ReadLinesAsync(string url)
{
var response = await _http.GetAsync(url);
await foreach (var line in response.Content.ReadAsAsyncEnumerable())
yield return line;
}
await foreach (var line in ReadLinesAsync("https://api.example.com/stream"))
Console.WriteLine(line);
11 What are Task, ValueTask, and the Task Parallel Library? ►
Async
// Task -- represents an async operation, heap-allocated
Task t1 = Task.Run(() => DoWork()); // fire-and-forget
Task<int> t2 = Task.Run(() => ComputeValue()); // with result
int result = await t2;
// ValueTask -- avoids allocation when result is synchronously available
// Use for high-frequency operations that often complete synchronously (caching, pooling)
public ValueTask<Product?> GetFromCacheAsync(int id)
{
if (_cache.TryGetValue(id, out var cached))
return ValueTask.FromResult<Product?>(cached); // synchronous, no allocation
return new ValueTask<Product?>(FetchFromDbAsync(id)); // async path
}
// Task Parallel Library -- CPU-bound parallel work
// Parallel.For / Parallel.ForEach
Parallel.For(0, 1000, i => ProcessItem(i));
Parallel.ForEach(items, new ParallelOptions { MaxDegreeOfParallelism = 4 }, item =>
ProcessItem(item));
// PLINQ -- parallel LINQ
var results = bigList.AsParallel()
.WithDegreeOfParallelism(4)
.Where(x => IsExpensive(x))
.Select(x => Transform(x))
.ToList();
// Task combinators
await Task.WhenAll(task1, task2, task3); // wait for ALL
var first = await Task.WhenAny(task1, task2); // first completed wins
// Task.Run vs async/await
// Task.Run = offload CPU-bound work to thread pool
// async/await = yield while waiting for I/O (no thread consumed during wait)
12 What are C# channels? How do they enable producer/consumer patterns? ►
Async System.Threading.Channels provides high-performance, thread-safe pipelines for passing data between producers and consumers asynchronously.
using System.Threading.Channels;
// Bounded channel -- limits backpressure
var channel = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait // producer waits if full
});
// Unbounded channel -- no backpressure (use with care)
var unbounded = Channel.CreateUnbounded<string>();
// Producer
async Task ProduceAsync(ChannelWriter<WorkItem> writer, CancellationToken ct)
{
try
{
for (int i = 0; i < 1000; i++)
{
await writer.WriteAsync(new WorkItem(i), ct); // backpressure: waits if full
}
}
finally
{
writer.Complete(); // signal no more items
}
}
// Consumer
async Task ConsumeAsync(ChannelReader<WorkItem> reader, CancellationToken ct)
{
await foreach (var item in reader.ReadAllAsync(ct))
{
await ProcessAsync(item);
}
}
// Wire up
var writer = channel.Writer;
var reader = channel.Reader;
await Task.WhenAll(
ProduceAsync(writer, CancellationToken.None),
ConsumeAsync(reader, CancellationToken.None),
ConsumeAsync(reader, CancellationToken.None) // multiple consumers
);
13 What is the lock statement and how do you manage thread synchronisation? ►
Threading
// lock -- mutual exclusion (Monitor.Enter/Exit under the hood)
private readonly object _syncRoot = new(); // dedicated lock object
public void Increment()
{
lock (_syncRoot) // only one thread enters at a time
{
_counter++;
}
}
// Interlocked -- atomic operations without locks (faster)
private int _counter;
Interlocked.Increment(ref _counter); // atomic ++
Interlocked.Add(ref _counter, 5); // atomic +=
Interlocked.CompareExchange(ref _counter, 10, 0); // CAS
// SemaphoreSlim -- limit concurrent access (async-aware)
var _semaphore = new SemaphoreSlim(initialCount: 3); // max 3 concurrent
async Task LimitedAccessAsync()
{
await _semaphore.WaitAsync(); // async-friendly -- won't block a thread
try { await DoWorkAsync(); }
finally { _semaphore.Release(); }
}
// ReaderWriterLockSlim -- multiple readers OR one writer
var _rwLock = new ReaderWriterLockSlim();
public string Read()
{
_rwLock.EnterReadLock();
try { return _data; }
finally { _rwLock.ExitReadLock(); }
}
public void Write(string data)
{
_rwLock.EnterWriteLock();
try { _data = data; }
finally { _rwLock.ExitWriteLock(); }
}
// Avoid lock on: this, string literals, Type objects
// Always lock on a private object
14 What are indexers in C#? ►
Syntax Indexers allow objects to be accessed using array-like [] notation. They are defined with the this keyword and any parameter type.
public class Matrix
{
private readonly double[,] _data;
public int Rows { get; }
public int Cols { get; }
public Matrix(int rows, int cols)
{
Rows = rows; Cols = cols;
_data = new double[rows, cols];
}
// Integer indexer
public double this[int row, int col]
{
get => _data[row, col];
set
{
if (row < 0 || row >= Rows) throw new IndexOutOfRangeException();
_data[row, col] = value;
}
}
}
var m = new Matrix(3, 3);
m[0, 0] = 1.0;
m[1, 1] = 5.0;
Console.WriteLine(m[0, 0]); // 1.0
// String indexer -- like a dictionary with fallback
public class Config
{
private readonly Dictionary<string, string> _values = new();
public string this[string key]
{
get => _values.TryGetValue(key, out var v) ? v : string.Empty;
set => _values[key] = value;
}
}
var cfg = new Config();
cfg["theme"] = "dark";
Console.WriteLine(cfg["theme"]); // "dark"
Console.WriteLine(cfg["missing"]); // "" (empty, not exception)
15 What is IAsyncEnumerable<T> and how does it work? ►
Async IAsyncEnumerable<T> (C# 8+) is the async counterpart of IEnumerable<T>. It allows asynchronously streaming data from a producer to a consumer without loading the entire dataset into memory.
// Async iterator -- yield return in an async method
public async IAsyncEnumerable<Product> StreamProductsAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
var page = 0;
while (true)
{
var batch = await _db.Products
.OrderBy(p => p.Id)
.Skip(page * 100)
.Take(100)
.ToListAsync(ct);
if (!batch.Any()) yield break;
foreach (var product in batch)
yield return product;
page++;
}
}
// Consume with await foreach
await foreach (var product in StreamProductsAsync(cancellationToken))
{
await ProcessProductAsync(product); // process one at a time, low memory
}
// WithCancellation
await foreach (var p in StreamProductsAsync().WithCancellation(ct))
Console.WriteLine(p.Name);
// Convert to list when needed
var all = await StreamProductsAsync().ToListAsync(); // System.Linq.Async package
// Use IAsyncEnumerable for:
// Streaming database results, paginated API responses, real-time events, large CSV/file reads
// Avoids: loading everything into memory before processing starts
16 What are anonymous types and the dynamic keyword? ►
Type System
// Anonymous type -- compiler-generated class with read-only properties
var product = new { Name = "Widget", Price = 9.99, InStock = true };
product.Name // "Widget"
product.Price // 9.99
// product.Price = 10; // ERROR -- anonymous types are immutable
// Common in LINQ projections
var results = people.Select(p => new { p.Name, AgeGroup = p.Age / 10 * 10 });
// dynamic -- bypasses compile-time type checking
dynamic value = 42;
value = "now a string"; // no error
value = new List<int>(); // no error
// Method calls resolved at runtime
dynamic obj = GetSomeDynamicObject();
obj.SomeMethod(); // resolved at runtime -- RuntimeBinderException if method doesn't exist
// Use case: COM interop, dynamic JSON, scripting integration
dynamic json = JsonConvert.DeserializeObject("{ \"name\": \"Alice\" }");
string name = json.name; // no casting needed
// AVOID dynamic in general code -- no IntelliSense, no compile errors, slower
// Use: generics, pattern matching, or explicit interfaces instead
// ExpandoObject -- dynamic dictionary-backed object
dynamic expando = new System.Dynamic.ExpandoObject();
expando.Name = "Alice"; // adds Name property dynamically
expando.Greet = (Action)(() => Console.WriteLine($"Hi, {expando.Name}!"));
expando.Greet();
17 What are expression-bodied members in C#? ►
Syntax Expression-bodied members use the => arrow to define a member as a single expression, reducing boilerplate for simple implementations.
public class Circle
{
private double _radius;
// Expression-bodied constructor (C# 7+)
public Circle(double radius) => _radius = radius;
// Expression-bodied property getter
public double Radius => _radius;
// Expression-bodied read-write property
public double Diameter
{
get => _radius * 2;
set => _radius = value / 2;
}
// Expression-bodied method
public double Area() => Math.PI * _radius * _radius;
public double Perimeter() => 2 * Math.PI * _radius;
// Expression-bodied override
public override string ToString() => $"Circle(r={_radius:F2})";
// Expression-bodied static method
public static Circle Unit() => new(1.0);
// Expression-bodied finaliser (destructor)
~Circle() => Console.WriteLine("Finalised");
}
// Using throw in expression body
public string Name
{
get => _name ?? throw new InvalidOperationException("Name not set");
set => _name = value ?? throw new ArgumentNullException(nameof(value));
}
// Rules:
// - void methods: single statement
// - Non-void methods: single expression (value is returned)
// - Properties: single expression for getter
18 What are C# 10/11/12 language features? ►
Modern C#
C# 10 (.NET 6): Global usings, file-scoped namespaces, record structs, const interpolated strings, CallerArgumentExpression
C# 11 (.NET 7): Required members, generic attributes, raw string literals ("""), list patterns, static abstract members in interfaces, nameof on instance members
C# 12 (.NET 8): Primary constructors, collection expressions, ref readonly parameters, Inline arrays, default lambda parameters, using aliases for any type
// C# 10 -- file-scoped namespace (no extra indentation)
namespace MyApp.Models;
public class User { public string Name { get; set; } = ""; }
// C# 11 -- required members
public class Config
{
public required string ConnectionString { get; init; }
public required int TimeoutSeconds { get; init; }
}
var cfg = new Config { ConnectionString = "...", TimeoutSeconds = 30 }; // required
// C# 11 -- raw string literals (no escape sequences needed)
var json = """
{
"name": "Alice",
"path": "C:\Users\Alice"
}
""";
// C# 12 -- primary constructors on classes
public class PersonService(IPersonRepository repo, ILogger<PersonService> logger)
{
public async Task<Person> GetAsync(int id)
{
logger.LogInformation("Getting person {Id}", id); // repo/logger as fields
return await repo.GetByIdAsync(id);
}
}
// C# 12 -- collection expressions
int[] nums = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob"];
Span<int> sp = [10, 20, 30];
int[] combined = [..nums, ..new[]{6,7}]; // spread operator
19 What is the Dispose pattern and SafeHandle? ►
Memory
// Full dispose pattern -- for classes with unmanaged resources
public class FileProcessor : IDisposable
{
private FileStream? _stream;
private bool _disposed;
public FileProcessor(string path)
=> _stream = File.OpenRead(path);
// Synchronous dispose
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this); // we already cleaned up, no need for finaliser
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
_stream?.Dispose(); // release managed resources (IDisposable objects)
// Release UNMANAGED resources here (if any)
_disposed = true;
}
~FileProcessor() // finaliser -- last resort cleanup (no guarantee of timing)
{
Dispose(disposing: false);
}
}
// IAsyncDisposable -- for async cleanup (database connections, async streams)
public class AsyncProcessor : IAsyncDisposable
{
private SqlConnection? _conn;
public async ValueTask DisposeAsync()
{
if (_conn != null) await _conn.DisposeAsync();
GC.SuppressFinalize(this);
}
}
await using var proc = new AsyncProcessor(...); // calls DisposeAsync on exit
// SafeHandle -- better than raw IntPtr for OS handles (P/Invoke)
// Automatically released on finalisation, even if Dispose not called
public class FileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public FileHandle() : base(ownsHandle: true) { }
protected override bool ReleaseHandle() => CloseHandle(handle);
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
}
20 What are the common C# design patterns and when do you use them? ►
Architecture
// Repository -- data access abstraction
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<List<Product>> GetAllAsync();
Task AddAsync(Product p);
Task UpdateAsync(Product p);
Task DeleteAsync(int id);
}
// Strategy -- swappable behaviour
public interface IDiscountStrategy { decimal Apply(decimal price); }
public class NoDiscount : IDiscountStrategy { public decimal Apply(decimal p) => p; }
public class PercentageDiscount : IDiscountStrategy
{
private readonly decimal _pct;
public PercentageDiscount(decimal pct) { _pct = pct; }
public decimal Apply(decimal p) => p * (1 - _pct);
}
// Observer -- via events
public class OrderService
{
public event EventHandler<Order>? OrderCreated;
public async Task<Order> CreateAsync(CreateOrderDto dto)
{
var order = await _repo.CreateAsync(dto);
OrderCreated?.Invoke(this, order); // notify subscribers
return order;
}
}
// Factory method
public static class LoggerFactory
{
public static ILogger Create(string type) => type switch
{
"file" => new FileLogger(),
"console" => new ConsoleLogger(),
_ => throw new ArgumentException($"Unknown: {type}")
};
}
// Builder pattern -- common with request builders
var request = new HttpRequestBuilder()
.WithMethod(HttpMethod.Post)
.WithUrl("/api/orders")
.WithBody(orderDto)
.WithHeader("Authorization", "Bearer " + token)
.Build();
21 What is the difference between == and .Equals() and ReferenceEquals()? ►
Equality
// == operator
// For reference types: reference equality by default
// For string: value equality (overridden by the compiler)
// For value types: value equality
// Can be overloaded
// .Equals() virtual method
// object.Equals: reference equality by default
// string, int, DateTime, etc. override to value equality
// Can be overridden in custom classes
// ReferenceEquals() -- ALWAYS reference equality, cannot be overridden
string a = "hello";
string b = "hello";
string c = new string("hello"); // force new heap object
Console.WriteLine(a == b); // True (string == is value)
Console.WriteLine(a.Equals(b)); // True (string Equals is value)
Console.WriteLine(ReferenceEquals(a, b)); // True (string interning)
Console.WriteLine(ReferenceEquals(a, c)); // False (different heap objects)
// Custom class -- override both for consistency
public class Money : IEquatable<Money>
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal a, string c) { Amount = a; Currency = c; }
public override bool Equals(object? obj)
=> obj is Money m && Equals(m);
public bool Equals(Money? other)
=> other is not null && Amount == other.Amount && Currency == other.Currency;
public override int GetHashCode() => HashCode.Combine(Amount, Currency);
// Overload == operator when overriding Equals
public static bool operator ==(Money? a, Money? b) => a?.Equals(b) ?? b is null;
public static bool operator !=(Money? a, Money? b) => !(a == b);
}
// Rules: if you override Equals, override GetHashCode too
// Objects that are equal MUST have the same hash code