C# backend interview prep

C# Interview Questions and Answers 2026

A focused C# interview set for backend developers. Each answer includes the reasoning interviewers expect, and many include small code examples you can discuss out loud.

Start C# mock interview Technical interview practice in EnglishA mode where non-native speakers can practice passing technical interviews.

Types and Language Basics

1How do value types differ from reference types?

Value type variables usually contain the value itself, so assignment copies the value. Reference type variables contain a reference to an object, so assignment copies the reference and both variables can observe mutations of the same object. The important nuance is that storage depends on lifetime and context, not just on the type: a value local to a method can live on the stack, while a value type stored as a class field, array element, closure capture, boxed value or async state-machine field lives with that object.

int a = 10;
int b = a;
b = 20;

// a is still 10

var first = new User { Name = "Ann" };
var second = first;
second.Name = "Kate";

// first.Name is also "Kate"

2How do class, struct and record differ?

A class is a reference type with object identity. A struct is a value type and is best for small immutable values such as coordinates, ranges or identifiers. A record class is a reference type with value-based equality; record struct is the value-type version. Records are compiler syntax over ordinary CLR types that generates equality, GetHashCode, ToString, deconstruction and copy support, including with expressions for non-destructive mutation. In backend code, records are useful for DTO-like models, while large mutable structs are risky because copying and modifying copies can surprise readers.

public record User(int Id, string Name);

var first = new User(1, "Alice");
var second = new User(1, "Alice");

Console.WriteLine(first == second); // true

3How do ref, out and in parameters differ?

ref passes an argument by reference and requires it to be initialized before the call; the method can read and change it. out also passes by reference, but the method must assign a value before returning. in passes a readonly byref, which can avoid copying for large structs, but it is not a deep immutability guarantee: mutable structs and non-readonly members can force defensive copies. At the IL level, ref, out and in are managed byref parameters with different C# rules.

void Increment(ref int value) => value++;

bool TryRead(string text, out int value) =>
    int.TryParse(text, out value);

double Length(in Vector vector) =>
    Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y);

4What are boxing and unboxing?

Boxing wraps a value type into a reference-typed object by allocating an object and copying the value into it. Unboxing extracts the value back and requires the exact compatible type. Frequent boxing adds allocations and garbage-collector pressure. Generics avoid many boxing cases because List<int> stores int values without converting each value to object.

int number = 42;
object boxed = number;      // boxing
int restored = (int)boxed;  // unboxing

object value = 42;
// long wrong = (long)value; // InvalidCastException
long ok = (long)(int)value;

5What are nullable value types and nullable reference types?

Nullable value types use Nullable<T> or T? and represent a real runtime wrapper for value types. Nullable reference types are mostly compiler analysis: string? tells the compiler a reference may be null, but it does not create a new runtime type. The null-forgiving operator ! only suppresses warnings; it does not protect from NullReferenceException.

int? age = null;

string name = "Alice";
string? optionalName = null;

var length = optionalName?.Length;
var actualName = optionalName ?? "Unknown";
optionalName ??= "Default";

string trusted = optionalName!;

6How do const, readonly and static readonly differ?

const is a compile-time constant limited to primitives, enum values, string or null, and its value is substituted into consuming assemblies during compilation. readonly is an instance field that can be assigned in a declaration or constructor. static readonly is one field per type, assigned in a declaration or static constructor. Public values that may change across library versions are safer as static readonly than public const, because clients can keep an old inlined const until they are recompiled.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);

7How do Equals, == and GetHashCode work together?

For ordinary classes, Equals and == compare references by default. Many built-in types and records compare by value. If Equals is overridden, GetHashCode must be consistent: if a.Equals(b) is true, both objects must have the same hash code. The reverse is not guaranteed. Never mutate fields used by equality or hashing while the object is a Dictionary key or HashSet element.

public sealed class User : IEquatable<User>
{
    public required int Id { get; init; }

    public bool Equals(User? other) =>
        other is not null && Id == other.Id;

    public override bool Equals(object? obj) =>
        obj is User other && Equals(other);

    public override int GetHashCode() =>
        Id.GetHashCode();
}

8How does override differ from new?

override changes virtual behavior, so the method called depends on the runtime type of the object. new hides a member from the base class, and the selected member depends on the static type of the variable. For polymorphism you normally want virtual and override; hiding with new is less common and can make behavior confusing.

class Base
{
    public virtual void First() => Console.WriteLine("Base.First");
    public void Second() => Console.WriteLine("Base.Second");
}

class Derived : Base
{
    public override void First() => Console.WriteLine("Derived.First");
    public new void Second() => Console.WriteLine("Derived.Second");
}

Base value = new Derived();
value.First();  // Derived.First
value.Second(); // Base.Second

9How does an abstract class differ from an interface?

An abstract class can contain state, constructors, fields, implemented methods, abstract methods and protected members. An interface describes a contract; modern interfaces can include default implementations and static abstract members, but they are still not the normal place for instance state. A class can inherit one base class but implement multiple interfaces. Use an abstract class for a shared base and shared implementation; use an interface for capabilities and boundaries.

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}

Generics, Collections and LINQ

10What are generics and why are they useful?

Generics let you write reusable type-safe code. They move many checks to compile time, remove manual casts and can avoid boxing for value types. Generic constraints describe what a type parameter must support, such as being a class, struct, interface implementation, base class, unmanaged type or having a public parameterless constructor.

public T? Find<T>(
    IEnumerable<T> items,
    Predicate<T> predicate)
{
    return items.FirstOrDefault(item => predicate(item));
}

public T Create<T>() where T : class, new()
{
    return new T();
}

11What are covariance and contravariance?

Covariance lets code use a more specific type where a more general produced type is expected, such as IEnumerable<string> as IEnumerable<object>. It is marked with out on interfaces and delegates. Contravariance goes the other direction for consumed values and is marked with in. Variance works only for interfaces and delegates with reference types; IEnumerable<int> cannot become IEnumerable<object> because that would require boxing, and List<string> is not assignable to List<object>.

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // covariance

Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // contravariance

interface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T value); }

12How does IEnumerable<T> differ from IQueryable<T>?

IEnumerable<T> represents .NET enumeration and LINQ operators usually take delegates such as Func<T, bool>. IQueryable<T> stores expression trees, for example Expression<Func<T, bool>>, that a provider such as Entity Framework Core can translate into SQL. Calling ToList too early materializes data and moves later filtering into memory. Also, not every C# expression can be translated by a query provider. Avoid leaking IQueryable through all application layers unless that is a deliberate design choice.

IEnumerable<User> loaded = loadedUsers;
var inMemoryAdults = loaded.Where(x => x.Age >= 18);

IQueryable<User> query = dbContext.Users;
var adults = await query
    .Where(x => x.Age >= 18)
    .ToListAsync();

13What is deferred execution in LINQ?

Many LINQ operators build a query without executing it immediately. Execution starts when the query is enumerated or when a terminal operation such as ToList, Count or First is called. This means source changes before enumeration can affect results, repeated enumeration can repeat work or SQL queries, and exceptions may occur during enumeration rather than query construction.

var query = numbers.Where(x => x > 10);

numbers.Add(42);

foreach (var number in query)
{
    Console.WriteLine(number);
}

var materialized = query.ToList();

14How do array, List<T>, Dictionary<TKey,TValue> and HashSet<T> differ?

An array has fixed size and fast indexed access. List<T> is a dynamic array with O(1) indexed access; Add at the end is amortized O(1) because growing capacity allocates a larger array and copies existing elements, while insertion in the middle is O(n). Dictionary<TKey,TValue> maps keys to values and usually provides O(1) key lookup. HashSet<T> stores unique elements and is ideal for membership checks. For hash-based collections, correct Equals and GetHashCode implementations are essential.

var ids = new HashSet<int> { 1, 2, 3 };

if (ids.Contains(userId))
{
    // Fast membership check
}

15How do IEnumerable<T>, ICollection<T>, IList<T> and IReadOnlyCollection<T> differ?

IEnumerable<T> promises only sequential enumeration. ICollection<T> adds Count and mutation methods such as Add and Remove. IList<T> adds indexed access. IReadOnlyCollection<T> exposes enumeration and Count but not mutation through that interface. API design should return the narrowest useful contract, but read-only interfaces do not guarantee deep immutability of the underlying object.

private readonly List<Order> _orders = new();

public IReadOnlyCollection<Order> Orders => _orders;

16How does yield return work?

yield return creates an iterator with deferred execution. The compiler transforms the method into a state machine, and values are produced one by one when MoveNext is called. This avoids intermediate collections and works well for large sequences. The traps are lifetime and multiple enumeration: do not return a lazy sequence that depends on a disposed DbContext or stream, and remember that Count followed by foreach can execute the iterator twice, repeating side effects or I/O.

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}

Delegates, Events and Exceptions

17What are delegates and lambda expressions?

A delegate is a type-safe reference to a method. Lambdas provide concise inline functions and are commonly assigned to Func, Action or Predicate delegates. Lambdas capture variables, not snapshots of their values, so a lambda can observe a later loop or variable value. If a lambda captures nothing, the compiler can cache a static delegate and avoid closure allocation; in performance-sensitive code, closures and extended object lifetimes can matter.

public delegate int Operation(int x, int y);

Operation operation = (x, y) => x + y;
Console.WriteLine(operation(2, 3));

int factor = 10;
Func<int, int> multiply = x => x * factor;

18How does event differ from delegate?

An event is usually backed by a delegate, but it restricts what outside code can do. External code can subscribe and unsubscribe, but only the owner class can raise the event. A public delegate field would allow callers to replace the invocation list or invoke it directly. Events also create references from publisher to subscriber, so long-lived publishers can cause memory leaks if subscribers do not unsubscribe.

public event EventHandler<OrderEventArgs>? OrderCreated;

service.OrderCreated += Handler;
service.OrderCreated -= Handler;

OrderCreated?.Invoke(this, args);

19How should exceptions be handled in C#?

Use exceptions for exceptional situations, not normal control flow. Catch only exceptions you can handle, avoid empty catch blocks, add context when wrapping and do not leak internal details to users. Use throw; to rethrow while preserving the original stack trace. If you wrap an exception, pass the original as InnerException, for example throw new DomainException("...", ex). Prefer TryParse-style APIs for expected failures.

try
{
    await service.ProcessAsync();
}
catch (ValidationException exception)
{
    // Expected domain error
}
finally
{
    // Cleanup
}

catch
{
    throw; // preserves stack trace
}

20What are exception filters?

Exception filters let a catch block run only when an additional condition is true. The filter is evaluated before stack unwinding and before entering the catch body, so when returns false the original stack remains intact for diagnostics and crash dumps. Filters are useful for HTTP status codes, database error codes, distinguishing transient errors and conditional logging. Keep filters simple and avoid complex side effects.

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}