Advanced C# Interview Questions and Answers

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

💻 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

📝 Knowledge Check

🧠 Quiz Question 1 of 5

What does the async/await state machine do when it encounters an await expression?





🧠 Quiz Question 2 of 5

What is the key difference between IEnumerable<T> and IQueryable<T> for database queries?





🧠 Quiz Question 3 of 5

What makes Span<T> different from a regular array or List<T> slice?





🧠 Quiz Question 4 of 5

When should you choose ValueTask over Task for async methods?





🧠 Quiz Question 5 of 5

What rule must you follow when overriding Equals() in C#?