.NET / C# Interview Questions: Junior Level (Q1–Q80)
80 C# and .NET interview questions for junior developers with detailed answers — covering C# basics, OOP, types, collections, LINQ, async/await, and ASP.NET Core fundamentals.
How to Use This Guide
These questions are asked in junior .NET developer interviews (0-2 years experience). Each includes a short answer for the first 30 seconds, and a full answer for when the interviewer asks you to go deeper.
C# Basics
Q1: What is the difference between int and Int32?
They are exactly the same. int is a C# keyword alias for System.Int32. Both are 32-bit signed integers. The same applies to string ↔ String, bool ↔ Boolean, double ↔ Double.
Q2: What is the difference between var and dynamic?
var: type is inferred at compile time. It's strongly typed — the compiler knows the type and provides IntelliSense and type-checking.dynamic: type is resolved at runtime. No compile-time checking. Used for COM interop, reflection, or working with JSON/scripts.
var x = 42; // compiled as int, fully type-safe
x = "hello"; // ERROR at compile time
dynamic d = 42;
d = "hello"; // fine at compile time, resolved at runtime
d.NonExistent(); // no compile error, RuntimeBinderException at runtimeQ3: What is the difference between const and readonly?
const: compile-time constant. Must be primitive or string. Value baked into IL. Can't be static (implicitly static).readonly: runtime constant. Can be any type, including objects. Set in declaration or constructor. Can be instance or static.
public const double Pi = 3.14159; // compile-time
public readonly DateTime StartTime = DateTime.UtcNow; // set once at runtimeQ4: What is boxing and unboxing?
Boxing: converting a value type (int, bool, struct) to object. Allocates heap memory.
Unboxing: extracting the value type back from object. Requires explicit cast.
int n = 42;
object boxed = n; // boxing — heap allocation
int unboxed = (int)boxed; // unboxing — explicit cast required
// Common pitfall: boxing in collections before generics
ArrayList list = new();
list.Add(42); // boxes every int
// Use List<int> instead — no boxingQ5: What are nullable value types?
Value types (int, bool, DateTime) can't normally be null. int? (which is Nullable<int>) wraps the value type and adds the ability to be null.
int? age = null;
if (age.HasValue)
Console.WriteLine(age.Value);
int result = age ?? 0; // null-coalescing
int result2 = age.GetValueOrDefault(0);Q6: What is string interpolation vs string.Format?
string name = "Alice";
int age = 25;
// Old way
string msg1 = string.Format("Name: {0}, Age: {1}", name, age);
// String interpolation (C# 6+) — more readable
string msg2 = $"Name: {name}, Age: {age}";
// Raw string literals (C# 11) — no escaping needed
string json = """
{
"name": "Alice",
"age": 25
}
""";Q7: What is the difference between == and .Equals() for strings?
For strings, both compare content (value equality) in most cases. However:
==is null-safe:null == nullistrue,null == "x"isfalse.Equals()throwsNullReferenceExceptionif called on null
string a = "hello";
string b = "hello";
Console.WriteLine(a == b); // true (content equality)
Console.WriteLine(a.Equals(b)); // true
string? c = null;
Console.WriteLine(c == null); // true (safe)
// Console.WriteLine(c.Equals("x")); // NullReferenceException!
Console.WriteLine("x".Equals(c)); // false (safe if called on non-null)Q8: What is a StringBuilder and when do you use it?
Strings in C# are immutable — every concatenation creates a new string object. StringBuilder is mutable and efficient for building strings in loops.
// BAD for large loops — creates many string objects
string result = "";
for (int i = 0; i < 10000; i++)
result += i; // 10,000 allocations!
// GOOD — single buffer, resizes as needed
var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
sb.Append(i);
string result = sb.ToString(); // one allocationObject-Oriented Programming
Q9: What are the four pillars of OOP?
- Encapsulation: hide internal state, expose only what's needed (private fields, public properties)
- Inheritance: derive new classes from existing ones (
class Dog : Animal) - Polymorphism: same interface, different behaviours (
override, virtual methods) - Abstraction: define what something does without specifying how (
abstract class,interface)
Q10: What is the difference between an abstract class and an interface?
| | Abstract Class | Interface | |--|---------------|-----------| | Multiple inheritance | No (C# allows only one base class) | Yes (implement many interfaces) | | Constructor | Yes | No | | Fields | Yes | No (properties only) | | Access modifiers | Yes | All public by default | | Default implementations | Yes | Yes (C# 8+) | | When to use | Shared base with state | Contract/capability |
// Use abstract class: Dog, Cat share Animal's Sleep() logic
public abstract class Animal
{
protected string Name;
public abstract void Speak();
public void Sleep() => Console.WriteLine($"{Name} is sleeping");
}
// Use interface: unrelated classes can all be serialized
public interface ISerializable
{
string Serialize();
}Q11: What is method overloading vs overriding?
- Overloading: same method name, different parameter signatures. Resolved at compile time.
- Overriding: redefine a virtual/abstract method in a derived class. Resolved at runtime.
// Overloading (compile-time)
void Print(string s) { }
void Print(int n) { }
void Print(string s, int n) { }
// Overriding (runtime)
public class Animal { public virtual void Speak() => Console.WriteLine("..."); }
public class Dog : Animal { public override void Speak() => Console.WriteLine("Woof"); }Q12: What is the difference between new and override on a method?
override: polymorphic replacement. The base class reference calls the derived method.new: hides the base method. The base class reference still calls the base method.
class Base { public virtual void M() => Console.WriteLine("Base"); }
class Override : Base { public override void M() => Console.WriteLine("Override"); }
class New : Base { public new void M() => Console.WriteLine("New"); }
Base b1 = new Override();
b1.M(); // "Override" — polymorphism works
Base b2 = new New();
b2.M(); // "Base" — new hides, doesn't replaceQ13: What is a sealed class?
Prevents inheritance from that class. Used when you don't want subclasses, or for performance (compiler can devirtualize calls).
public sealed class SqlCustomerRepository : ICustomerRepository
{
// Nobody can inherit from this
}Q14: What is the this keyword?
Refers to the current instance of the class. Used to:
- Disambiguate fields from parameters (
this.name = name) - Call another constructor (
this(param)) - Return the current instance for method chaining
public class Builder
{
private string _name = "";
private int _age;
public Builder WithName(string name) { _name = name; return this; }
public Builder WithAge(int age) { _age = age; return this; }
public Person Build() => new Person(_name, _age);
}
var person = new Builder().WithName("Alice").WithAge(25).Build();Q15: What is a struct vs class?
| | Struct | Class |
|--|-------|-------|
| Type | Value type | Reference type |
| Memory | Stack (usually) | Heap |
| Assignment | Copies value | Copies reference |
| Inheritance | Cannot inherit | Can inherit |
| Null | Can't be null (unless Nullable<T>) | Can be null |
| Best for | Small, immutable data (Point, Color) | Most objects |
public struct Point { public double X, Y; }
Point a = new Point { X = 1, Y = 2 };
Point b = a; // COPY
b.X = 99;
Console.WriteLine(a.X); // still 1 — a is unaffectedQ16: What is the difference between a shallow copy and a deep copy?
- Shallow copy: copies the object, but reference-type fields still point to the same objects
- Deep copy: copies the object and recursively copies all referenced objects
// Shallow copy (records use this with 'with')
var dto1 = new OrderDto(1, "Alice", items);
var dto2 = dto1 with { Id = 2 }; // items list is still shared!
// Deep copy — manual or via serialization
var json = JsonSerializer.Serialize(original);
var deepCopy = JsonSerializer.Deserialize<OrderDto>(json)!;Collections
Q17: What is the difference between Array, List<T>, and IEnumerable<T>?
Array: fixed size, fastest random access, contiguous memoryList<T>: dynamic size, backed by array, supports Add/RemoveIEnumerable<T>: read-only, forward-only, lazy. Interface that all collections implement.
// Array: fixed size
int[] arr = new int[5];
arr[0] = 1;
// List<T>: dynamic
var list = new List<int> { 1, 2, 3 };
list.Add(4);
list.Remove(2);
// IEnumerable<T>: lazy, composable
IEnumerable<int> evens = list.Where(n => n % 2 == 0); // not evaluated yetQ18: When would you use Dictionary vs List?
Dictionary<K,V>: O(1) lookup by key. Use when you need fast access by a specific identifier.List<T>: O(n) search, O(1) access by index. Use when you iterate in order.
// Dictionary: fast lookup by id
var products = new Dictionary<int, Product>();
var p = products[42]; // O(1)
// List: ordered collection
var ordered = new List<Order>(); // iterate in insertion orderQ19: What is IEnumerable vs IQueryable?
IEnumerable<T>: in-memory operations. LINQ runs in C# on fetched data.IQueryable<T>: translatable queries. LINQ translates to SQL (or other query language) and runs on the data source.
// IQueryable — the WHERE runs in SQL (efficient)
var orders = _context.Orders
.Where(o => o.Status == "pending") // translated to SQL WHERE
.ToList();
// IEnumerable — all orders fetched, then filtered in memory (inefficient!)
IEnumerable<Order> query = _context.Orders.AsEnumerable();
var orders = query.Where(o => o.Status == "pending").ToList();LINQ
Q20: What is deferred vs immediate execution in LINQ?
- Deferred: query is not executed until you iterate it (e.g.,
foreach,ToList()) - Immediate: executed right away (
ToList(),Count(),First(),Sum())
var query = numbers.Where(n => n > 5); // deferred — no execution
numbers.Add(10);
var result = query.ToList(); // executed NOW — includes 10
// Immediate execution
int count = numbers.Where(n => n > 5).Count(); // runs nowQ21: What is the difference between First(), FirstOrDefault(), Single(), and SingleOrDefault()?
| Method | No match | Multiple matches |
|--------|---------|-----------------|
| First() | throws | returns first |
| FirstOrDefault() | returns default (null/0) | returns first |
| Single() | throws | throws |
| SingleOrDefault() | returns default | throws |
Use Single when you expect exactly one result and want an error if that assumption is wrong.
Q22: What is LINQ method syntax vs query syntax?
// Method syntax (most common)
var result = orders
.Where(o => o.Total > 100)
.OrderBy(o => o.CreatedAt)
.Select(o => o.Id);
// Query syntax (SQL-like, less common in practice)
var result = from o in orders
where o.Total > 100
orderby o.CreatedAt
select o.Id;Q23: How does GroupBy work in LINQ?
var ordersByStatus = orders
.GroupBy(o => o.Status)
.Select(g => new
{
Status = g.Key,
Count = g.Count(),
Total = g.Sum(o => o.Total)
})
.ToList();
// Result: [{ Status="pending", Count=5, Total=500 }, ...]Async / Await
Q24: What does async/await actually do?
await pauses the method and returns control to the caller while waiting for the task to complete. The thread is freed to do other work. When the awaited task finishes, the method resumes.
What it does NOT do: create new threads. Async code is about not blocking threads — not about parallelism.
public async Task<string> GetDataAsync()
{
// Thread is freed during the HTTP call
string data = await httpClient.GetStringAsync(url);
// Method resumes here when HTTP call completes
return data.ToUpper();
}Q25: What is the difference between Task and ValueTask?
Task<T>: always allocates on the heap. Use for most async methods.ValueTask<T>: can avoid allocation if result is available synchronously. Use in hot code paths (e.g., cache hits) where the operation is often synchronous.
Q26: What happens if you don't await an async method?
The method starts but you don't wait for it to complete. Exceptions are silently swallowed.
// WRONG — no await, exception swallowed
DoWorkAsync();
// CORRECT — await it
await DoWorkAsync();
// If intentional fire-and-forget, suppress the warning:
_ = Task.Run(() => DoWorkAsync());Q27: What is ConfigureAwait(false) and when should you use it?
By default, await captures the current synchronization context and resumes on it. In a web server (ASP.NET Core), there's no synchronization context, so it doesn't matter. But in UI frameworks (WPF, WinForms) or older ASP.NET, it can cause deadlocks.
In library code: use ConfigureAwait(false) to avoid capturing context.
In application/controller code: don't bother — ASP.NET Core has no sync context.
Q28: What is Task.WhenAll vs Task.WhenAny?
// WhenAll: wait for all tasks, run concurrently
var (users, orders) = await (
GetUsersAsync(), // both start immediately
GetOrdersAsync() // runs concurrently with GetUsersAsync
);
// WhenAny: continue when the first task completes
var fastResult = await Task.WhenAny(task1, task2, task3);ASP.NET Core
Q29: What is the difference between AddTransient, AddScoped, and AddSingleton?
- Transient: new instance every time the service is resolved
- Scoped: one instance per HTTP request (scope)
- Singleton: one instance for the entire application lifetime
services.AddTransient<IEmailSender, SmtpEmailSender>(); // new each time
services.AddScoped<IOrderRepository, SqlOrderRepository>(); // new per request
services.AddSingleton<IConfiguration>(...); // one foreverCommon rule: DbContext = Scoped, HTTP clients = registered as factory/typed client.
Q30: What is middleware in ASP.NET Core?
Middleware is a component in the request pipeline. Each piece of middleware can:
- Process the incoming request
- Pass the request to the next middleware (
await next(context)) - Process the outgoing response
Common middleware: authentication, CORS, routing, logging, error handling.
Q31: What is the difference between IActionResult and ActionResult<T>?
IActionResult: non-generic, can return any result typeActionResult<T>: generic, gives Swagger/OpenAPI accurate response type information
// IActionResult
public IActionResult Get(int id) => id > 0 ? Ok(new Product()) : NotFound();
// ActionResult<T> — Swagger knows it returns Product on 200
public ActionResult<Product> Get(int id) => id > 0 ? new Product() : NotFound();Q32: What is model binding in ASP.NET Core?
Automatic extraction of values from HTTP requests into method parameters.
[HttpGet("{id}")]
public IActionResult Get(
int id, // from route
[FromQuery] string? filter, // from query string
[FromHeader] string? apiKey, // from header
[FromBody] UpdateRequest request, // from JSON body
CancellationToken ct) // injected by frameworkQ33: What is [ApiController] attribute?
Enables:
- Automatic model validation (returns 400 if model state is invalid)
- Automatic
[FromBody]on complex POST parameters - Better problem details responses
Q34: What is ILogger and how do you use it?
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger) => _logger = logger;
public async Task ProcessAsync(int orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
try { /* ... */ }
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
}
}Log levels: LogTrace → LogDebug → LogInformation → LogWarning → LogError → LogCritical
Use structured logging parameters ({OrderId}) not string interpolation — allows log systems to index by property.
Q35: What is dependency injection and why is it useful?
DI is a pattern where a class receives its dependencies from outside rather than creating them itself.
Benefits:
- Testability: inject mock implementations in tests
- Loose coupling: depend on interfaces, not concrete classes
- Lifetime management: framework manages object creation and disposal
// Without DI — tightly coupled, hard to test
public class OrderService
{
private readonly SmtpEmailSender _email = new SmtpEmailSender("smtp.gmail.com");
}
// With DI — loosely coupled, easy to test
public class OrderService
{
private readonly IEmailSender _email;
public OrderService(IEmailSender email) => _email = email;
}Q36–Q80: Quick-fire Questions
Q36: What is the difference between throw and throw ex?
throw re-throws the current exception preserving the original stack trace. throw ex resets the stack trace to the current location — you lose where the exception originally happened. Always use throw.
Q37: What is a using statement?
Automatically calls Dispose() on objects that implement IDisposable when the block exits, even on exceptions.
using (var stream = File.OpenRead("file.txt")) { ... }
// or modern style:
using var stream = File.OpenRead("file.txt");
// Disposed at end of enclosing scopeQ38: What is IDisposable?
An interface with one method: void Dispose(). Implement it when your class holds unmanaged resources (file handles, database connections, HTTP clients). The using statement calls it automatically.
Q39: What is the difference between Dispose() and garbage collection?
GC frees managed memory automatically. Dispose() frees unmanaged resources (files, connections) immediately when you're done. Don't wait for GC to release a database connection — Dispose() it right away.
Q40: What is a static class? A class that cannot be instantiated and only has static members. Used for utility/extension methods and global state.
public static class MathUtils
{
public static double Square(double x) => x * x;
}Q41: What are extension methods? Methods that extend a type without modifying it.
public static class StringExtensions
{
public static bool IsValidEmail(this string email)
=> email.Contains('@') && email.Contains('.');
}
// Usage: "alice@example.com".IsValidEmail()Q42: What is a lambda expression?
An anonymous function using the => arrow syntax.
Func<int, int> square = x => x * x;
var evens = numbers.Where(n => n % 2 == 0);Q43: What is a delegate?
A type-safe function pointer. Func<T, TResult>, Action<T>, and Predicate<T> are built-in delegate types.
Q44: What is SOLID? 5 object-oriented design principles:
- Single Responsibility — one reason to change
- Open/Closed — open for extension, closed for modification
- Liskov Substitution — subtypes must be substitutable for base types
- Interface Segregation — many specific interfaces > one general
- Dependency Inversion — depend on abstractions, not concretions
Q45: What is a factory pattern? An object that creates other objects. Decouples creation from usage.
public interface IEmailSenderFactory { IEmailSender Create(string provider); }Q46: What is a singleton pattern?
Ensures only one instance of a class exists. In .NET: register with AddSingleton or use a static field with lazy initialization.
Q47: What is Lazy<T>?
Thread-safe lazy initialization — the value is only created when first accessed.
private static readonly Lazy<ExpensiveResource> _resource =
new(() => new ExpensiveResource());
ExpensiveResource r = _resource.Value; // created on first accessQ48: What is an enum? A named set of integral constants.
public enum OrderStatus { Pending, Processing, Shipped, Delivered, Cancelled }
OrderStatus status = OrderStatus.Pending;
// Can convert: (int)status = 0, Enum.Parse<OrderStatus>("Pending")Q49: What is a partial class? A class definition split across multiple files. All parts must be in the same assembly. EF Core uses this for generated code.
// File1.cs
public partial class Customer { public string Name { get; set; } = ""; }
// File2.cs
public partial class Customer { public string GetDisplay() => Name; }Q50: What is nameof()?
Returns the name of a variable, type, or member as a string. Compile-time safe (refactor-proof).
throw new ArgumentNullException(nameof(customer));
// Instead of: throw new ArgumentNullException("customer");Q51: What is the difference between IEnumerable<T> and ICollection<T>?
IEnumerable<T>: read-only, forward-only iteration. ICollection<T> extends it with Count, Add, Remove, Contains.
Q52: What is an anonymous type? A type without a name, defined inline.
var summary = new { Name = "Alice", Total = 999.99 };
Console.WriteLine(summary.Name);Q53: What is a tuple? A lightweight data structure for grouping values without a class.
(string name, int age) = ("Alice", 25);
var result = (Min: 1, Max: 100);
Console.WriteLine(result.Min);Q54: What is pattern matching? Testing a value against a pattern and extracting data from it.
if (obj is string s && s.Length > 5) Console.WriteLine(s);
string msg = shape switch { Circle c => $"r={c.Radius}", _ => "other" };Q55: What is string comparison best practice?
Use StringComparison.OrdinalIgnoreCase for case-insensitive checks. Don't use ToUpper() or ToLower() for comparisons — it allocates a new string.
if (status.Equals("active", StringComparison.OrdinalIgnoreCase)) { }Q56: What is Environment.NewLine vs \n?
Environment.NewLine is platform-specific (\r\n on Windows, \n on Linux/Mac). For web/network protocols, always use \n or \r\n explicitly.
Q57: What is Math.Ceiling, Math.Floor, and Math.Round?
Floor: round down (4.9 → 4)Ceiling: round up (4.1 → 5)Round(x, 2, MidpointRounding.AwayFromZero): standard rounding
Q58: How do you convert a string to an integer?
int n = int.Parse("42"); // throws on invalid
bool ok = int.TryParse("42", out n); // safe
int n2 = Convert.ToInt32("42"); // Convert.ToInt32(null) returns 0Q59: What is the checked keyword?
Makes arithmetic throw OverflowException on integer overflow (normally silently wraps).
checked { int result = int.MaxValue + 1; } // throws OverflowExceptionQ60: What is object.ReferenceEquals?
Checks if two references point to the same object (ignoring any == overload).
string a = "hello";
string b = new string("hello");
Console.WriteLine(object.ReferenceEquals(a, b)); // false (different objects)
Console.WriteLine(a == b); // true (same content)Q61: What is string interning?
.NET maintains a pool of string literals. Identical string literals share the same memory. string.Intern(s) manually interns a string.
Q62: What is a Span<T>?
A stack-allocated, type-safe window over memory (array slice). Used in high-performance code to avoid allocations.
string text = "Hello, World!";
ReadOnlySpan<char> slice = text.AsSpan(0, 5); // "Hello" — no allocationQ63: What is Memory<T> vs Span<T>?
Span<T> is stack-only — can't be stored in a class field or used across await. Memory<T> can be stored and used asynchronously.
Q64: What is the difference between == and Object.Equals for value types?
For value types (structs, primitives), == compares values. Object.Equals does the same unless overridden. For classes, == compares references unless overloaded.
Q65: What is IComparable<T> vs IEquatable<T>?
IComparable<T>: supports ordering (<, >, sorting). IEquatable<T>: supports equality comparison without boxing.
Q66: What is Enumerable.Range?
Generates a sequence of integers.
var numbers = Enumerable.Range(1, 10); // 1,2,3,...,10
var squares = Enumerable.Range(1, 10).Select(n => n * n);Q67: What is the difference between Concat, Append, and Union?
Concat: combines two sequences (keeps duplicates)Append: adds one element to the endUnion: combines and removes duplicates
Q68: What is SelectMany?
Flattens a sequence of sequences.
var allItems = orders.SelectMany(o => o.Items); // all items from all orders in one listQ69: What is Zip in LINQ?
Combines two sequences element-by-element.
var combined = names.Zip(ages, (name, age) => $"{name} is {age}");Q70: What is Aggregate in LINQ?
Applies an accumulator function over a sequence.
int product = new[] { 1,2,3,4,5 }.Aggregate((acc, x) => acc * x); // 120Q71: What is the ?: ternary operator?
string result = age >= 18 ? "Adult" : "Minor";Q72: What is the difference between as and explicit cast (T)?
(T)obj: throwsInvalidCastExceptionif cast failsobj as T: returns null if cast fails (only for reference types and nullable)
var s = obj as string; // null if not a string
var s2 = (string)obj; // throws if not a stringQ73: What is Convert.ToInt32 vs int.Parse vs (int)?
int.Parse("42"): string to int, throws on null or invalidConvert.ToInt32("42"): string to int, returns 0 on null, throws on invalid(int)3.9: truncates double to int (3)
Q74: What does params keyword do?
Allows a method to accept variable number of arguments as an array.
static int Sum(params int[] numbers) => numbers.Sum();
Sum(1, 2, 3); // works
Sum(new[] { 1, 2, 3 }); // also worksQ75: What is yield return?
Creates a lazy iterator — values are produced one at a time on demand.
IEnumerable<int> GetEvens(int max)
{
for (int i = 0; i <= max; i += 2)
yield return i; // returns one at a time
}Q76: What is the difference between async void and async Task?
async void can't be awaited and exceptions are unhandled. Only use for event handlers. Always use async Task otherwise.
Q77: What is CancellationToken?
A token that signals a cancellation request. Pass it through the call chain so operations can stop early (e.g., user navigates away, request timeout).
app.MapGet("/data", async (CancellationToken ct) =>
{
await _service.GetDataAsync(ct); // stops if request is cancelled
});Q78: What is IHostedService?
Interface for background services in ASP.NET Core — runs on a background thread during app lifetime.
Q79: What is the difference between HttpClient and IHttpClientFactory?
new HttpClient() in loops causes socket exhaustion. IHttpClientFactory manages a pool of HTTP handlers and reuses them efficiently.
// Register: services.AddHttpClient<WeatherService>();
// Inject: private readonly HttpClient _client;Q80: What is the [Required] attribute?
A data annotation that marks a property as required for model validation. Works with model binding validation in ASP.NET Core.
Entity Framework Core
Q81: What is Entity Framework Core and what problem does it solve?
EF Core is an ORM (Object-Relational Mapper) — it maps C# classes to database tables and translates LINQ queries to SQL. You write C# instead of SQL, and EF Core handles connection management, command creation, and materialising results into objects.
// Without EF Core — manual SQL
using var conn = new SqlConnection(connectionString);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT Id, Name FROM Customers WHERE Id = @id";
cmd.Parameters.AddWithValue("@id", customerId);
// ... read with SqlDataReader, map manually ...
// With EF Core — LINQ translates to the same SQL
var customer = await context.Customers.FindAsync(customerId);Q82: What is a DbContext?
DbContext is the primary class in EF Core. It represents a session with the database and provides:
DbSet<T>properties — one per entity, used for querying and savingSaveChangesAsync()— persists all tracked changes in one transactionOnModelCreating()— configure entity mappings
public class AppDbContext(DbContextOptions<AppDbContext> opts) : DbContext(opts)
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnModelCreating(ModelBuilder model)
{
model.Entity<Order>().HasKey(o => o.Id);
model.Entity<Order>().Property(o => o.Total).HasPrecision(18, 2);
}
}Q83: What is a migration in EF Core?
A migration is a C# file that describes a schema change. EF Core compares your model to the current schema and generates the migration automatically.
# Create a migration after changing your model
dotnet ef migrations add AddOrderStatus
# Apply migrations to the database
dotnet ef database update// Generated migration file
public partial class AddOrderStatus : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Status", table: "Orders", nullable: false, defaultValue: "Pending");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "Status", table: "Orders");
}
}Q84: What is the difference between eager loading, lazy loading, and explicit loading?
All three load related entities, but at different times and in different ways.
// EAGER LOADING — Include() loads related data in the same SQL query
var orders = await context.Orders
.Include(o => o.Customer) // JOIN in one query
.Include(o => o.Items) // another JOIN
.ToListAsync();
// LAZY LOADING — related data loaded on first access (requires proxy)
// Needs: UseLazyLoadingProxies() in configuration
// Not recommended for web APIs — causes N+1 queries unintentionally
// EXPLICIT LOADING — load related data on demand
var order = await context.Orders.FindAsync(orderId);
await context.Entry(order).Reference(o => o.Customer).LoadAsync();
await context.Entry(order).Collection(o => o.Items).LoadAsync();Q85: What is the N+1 query problem?
Fetching a list of N entities and then making one additional query per entity to load a related property — N+1 total queries instead of 1.
// N+1 PROBLEM:
var orders = await context.Orders.ToListAsync(); // 1 query
foreach (var order in orders)
{
Console.WriteLine(order.Customer.Name); // 1 query PER ORDER = N+1 queries!
}
// FIX: eager loading with Include — 1 query with JOIN
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync();Q86: What is AsNoTracking() in EF Core?
By default, EF Core tracks every loaded entity in its change tracker (to detect modifications for SaveChanges). For read-only queries where you will never modify the data, AsNoTracking() skips this overhead — 20-30% faster.
// Read-only query — no need to track changes
var orders = await context.Orders
.AsNoTracking()
.Where(o => o.Status == "Paid")
.ToListAsync();
// Use for: GET endpoints, reports, dashboards
// Do NOT use for: data you plan to modify and saveQ87: What is the difference between Add, Update, and Attach in EF Core?
Add: marks entity as Added — EF Core will INSERT it on SaveChangesUpdate: marks entity (and all its properties) as Modified — EF Core will UPDATE all columnsAttach: adds entity to the context in Unchanged state — useful when you have an entity with an ID but didn't load it from the database
// Add — INSERT
var order = new Order { CustomerId = 1, Total = 99.99m };
context.Orders.Add(order);
await context.SaveChangesAsync(); // generates INSERT
// Update — UPDATE all columns (even unchanged ones)
order.Status = "Shipped";
context.Orders.Update(order);
await context.SaveChangesAsync(); // generates UPDATE with ALL columns
// Better for partial update: change tracked entity
var tracked = await context.Orders.FindAsync(id);
tracked!.Status = "Shipped";
await context.SaveChangesAsync(); // generates UPDATE with only changed columnsQ88: What are data annotations vs Fluent API in EF Core?
Both configure how entities map to the database. Fluent API (in OnModelCreating) is preferred because it keeps domain classes free of infrastructure concerns.
// Data annotations — attributes on the class
public class Order
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Description { get; set; } = "";
[Column(TypeName = "decimal(18,2)")]
public decimal Total { get; set; }
}
// Fluent API — in OnModelCreating (preferred)
model.Entity<Order>(entity =>
{
entity.HasKey(o => o.Id);
entity.Property(o => o.Description).IsRequired().HasMaxLength(200);
entity.Property(o => o.Total).HasColumnType("decimal(18,2)");
});Q89: What is a DbContext lifetime and why should it be Scoped?
DbContext tracks entities in memory (the change tracker). It should be Scoped (one per HTTP request) because:
- Shorter than Singleton: a Singleton DbContext accumulates tracked entities indefinitely and has connection pool issues
- Longer than Transient: creating a new DbContext per service call loses change tracking and causes multiple transactions
// Register as Scoped (default for AddDbContext)
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(connectionString));
// Equivalent to: builder.Services.AddScoped<AppDbContext>();
// Never inject a Scoped DbContext into a Singleton — captive dependency bugQ90: What does SaveChanges do internally?
EF Core examines the change tracker, generates SQL for all Added/Modified/Deleted entities, executes them in a single database transaction, and clears the change tracker state. If any SQL fails, the transaction is rolled back and nothing is committed.
// Multiple operations — committed atomically
context.Orders.Add(newOrder);
context.Customers.Update(existingCustomer);
context.AuditLogs.Add(new AuditLog("Order created"));
await context.SaveChangesAsync(); // one transaction: INSERT + UPDATE + INSERT
// If any fails → all roll backDependency Injection Deep Dive
Q91: What is the Service Locator anti-pattern and why should you avoid it?
Service Locator means calling serviceProvider.GetService<T>() inside a class to get dependencies instead of injecting them via the constructor. It hides dependencies, makes the class hard to test, and couples code to the DI container.
// BAD: Service Locator — hidden dependency
public class OrderService
{
private readonly IServiceProvider _sp;
public OrderService(IServiceProvider sp) => _sp = sp;
public void Process()
{
var repo = _sp.GetService<IOrderRepository>(); // hidden dependency
// caller can't see that this class needs IOrderRepository
}
}
// GOOD: constructor injection — explicit, testable
public class OrderService(IOrderRepository repo)
{
public void Process() => repo.DoWork();
}Q92: What is a captive dependency?
A captive dependency is when a shorter-lived service is injected into a longer-lived service. The longer-lived service captures the shorter-lived one for its entire lifetime — bypassing the intended lifetime.
// PROBLEM: Singleton captures Scoped → Scoped service lives as long as Singleton
public class OrderProcessor // Singleton
{
private readonly IOrderRepository _repo; // Scoped — WRONG!
public OrderProcessor(IOrderRepository repo) => _repo = repo;
// _repo is created once and never replaced — shares state across all requests
}
// FIX 1: Make OrderProcessor Scoped too
// FIX 2: Use IServiceScopeFactory to create a scope inside the Singleton
public class OrderProcessor(IServiceScopeFactory scopeFactory)
{
public async Task ProcessAsync()
{
using var scope = scopeFactory.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
await repo.DoWorkAsync();
}
}Q93: What is the difference between GetService<T> and GetRequiredService<T>?
GetService<T>: returnsnullif the service is not registeredGetRequiredService<T>: throwsInvalidOperationExceptionif not registered
Use GetRequiredService<T> in production code — fail fast at startup rather than getting a NullReferenceException later.
Q94: What are keyed services in .NET 8?
Multiple implementations of the same interface registered under different keys. Useful when you need to pick a specific implementation at runtime.
// Register two implementations of IEmailSender with different keys
builder.Services.AddKeyedSingleton<IEmailSender, SmtpEmailSender>("smtp");
builder.Services.AddKeyedSingleton<IEmailSender, SendGridEmailSender>("sendgrid");
// Inject a specific one using [FromKeyedServices]
public class NotificationService(
[FromKeyedServices("sendgrid")] IEmailSender emailSender)
{ }Q95: How do you validate that all services are registered correctly?
// .NET 6+: ValidateOnBuild checks registrations at startup
builder.Services.AddDbContext<AppDbContext>(...);
// ... other registrations
var app = builder.Build();
// If any required dependency is missing → throws at startup, not at runtime// Or: call IServiceProvider.GetRequiredService in a startup check
using var scope = app.Services.CreateScope();
scope.ServiceProvider.GetRequiredService<AppDbContext>(); // throws if missingException Handling
Q96: What is the difference between try/catch/finally and using?
finally always runs (even on exception). using is syntactic sugar for try/finally with Dispose() in the finally block.
// using is equivalent to:
using var conn = new SqlConnection(cs);
// ...
// Equivalent to:
var conn = new SqlConnection(cs);
try { /* ... */ }
finally { conn.Dispose(); }
// Both guarantee Dispose is called even on exceptionQ97: When should you catch exceptions vs let them propagate?
Catch exceptions when you can meaningfully handle them (retry, fallback, logging + rethrow). Let them propagate to a global handler (IExceptionHandler, middleware) when you cannot add value at the current level.
// GOOD: handle and retry
public async Task<string> GetWithRetryAsync(CancellationToken ct)
{
for (int attempt = 0; attempt < 3; attempt++)
{
try { return await _client.GetStringAsync("/data", ct); }
catch (HttpRequestException) when (attempt < 2)
{
await Task.Delay(TimeSpan.FromSeconds(attempt + 1), ct);
}
}
throw new ServiceUnavailableException("Max retries exceeded");
}
// BAD: catch-and-swallow — hides bugs
catch (Exception) { } // silently swallow all exceptionsQ98: What is an AggregateException?
Wraps one or more exceptions thrown by parallel or async operations. Task.WhenAll throws AggregateException when multiple tasks fail.
try
{
await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex) when (ex is AggregateException agg)
{
foreach (var inner in agg.InnerExceptions)
logger.LogError(inner, "Task failed");
}
// Or access individual task exceptions:
var t1 = Task.FromException(new InvalidOperationException("t1 failed"));
await Task.WhenAll(t1, task2);
// t1.Exception?.InnerException — the original exceptionQ99: What is ProblemDetails in ASP.NET Core?
A standardised JSON error response format (RFC 9457). Controllers and middleware return ProblemDetails so clients have a consistent error structure to parse.
// Register
builder.Services.AddProblemDetails();
// Return from controller
return Problem(
title: "Order not found",
detail: $"Order {id} does not exist.",
statusCode: 404);
// JSON response:
// { "type": "...", "title": "Order not found", "status": 404, "detail": "..." }Q100: What is the difference between Exception, ApplicationException, and custom exceptions?
ApplicationException was intended for application-level exceptions but Microsoft no longer recommends it — it adds no value. Create your own exception hierarchy directly from Exception or a meaningful base.
// Custom exception hierarchy
public class DomainException(string message) : Exception(message) { }
public class NotFoundException(string resource, object key)
: DomainException($"{resource} with key '{key}' was not found.") { }
public class ValidationException(IEnumerable<string> errors)
: DomainException("Validation failed")
{
public IReadOnlyList<string> Errors { get; } = errors.ToList();
}Testing
Q101: What is the difference between a unit test, integration test, and end-to-end test?
Unit test: tests one class/method in isolation; all dependencies mocked
Integration test: tests multiple components together (real DB, real HTTP)
End-to-end test: tests the full system through the UI or API entry point
Pyramid: many unit tests (fast, cheap), fewer integration tests, few E2E tests (slow, expensive)Q102: What is xUnit and how does it differ from NUnit and MSTest?
All three are .NET testing frameworks. xUnit is the most modern:
- No
[TestFixture]class attribute needed - Constructor injection for test dependencies (no
[SetUp]) IAsyncLifetimefor async setup/teardown- Parallel test execution by default
// xUnit test
public class OrderTests
{
[Fact]
public void Create_ValidData_ReturnsOrder()
{
var order = Order.Create(1, [new OrderItem(1, 1, 9.99m)]);
Assert.Equal(OrderStatus.Pending, order.Status);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void Create_InvalidCustomerId_Throws(int customerId)
{
Assert.Throws<ArgumentOutOfRangeException>(
() => Order.Create(customerId, [new OrderItem(1, 1, 9.99m)]));
}
}Q103: What is NSubstitute and how do you use it?
NSubstitute is a mocking library. It creates fake implementations of interfaces so you can test a class without real dependencies.
// Create a mock
var repo = Substitute.For<IOrderRepository>();
// Configure return value
repo.GetByIdAsync(42, Arg.Any<CancellationToken>())
.Returns(new Order { Id = 42, Status = "Pending" });
// Use in the system under test
var handler = new GetOrderHandler(repo);
var result = await handler.Handle(new GetOrderQuery(42), CancellationToken.None);
// Verify the mock was called
await repo.Received(1).GetByIdAsync(42, Arg.Any<CancellationToken>());Q104: What is the AAA pattern in tests?
Arrange-Act-Assert — the standard structure for a test:
[Fact]
public async Task PlaceOrder_ValidCommand_ReturnsOrderId()
{
// ARRANGE — set up the dependencies and input
var repo = Substitute.For<IOrderRepository>();
var command = new PlaceOrderCommand(42, [new OrderItemDto(1, 2, 9.99m)]);
var handler = new PlaceOrderHandler(repo);
// ACT — call the system under test
var orderId = await handler.Handle(command, CancellationToken.None);
// ASSERT — verify the expected outcome
Assert.True(orderId > 0);
await repo.Received(1).AddAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>());
}Q105: What is WebApplicationFactory in ASP.NET Core testing?
It starts your entire ASP.NET Core application in memory so you can send real HTTP requests to it in tests — without a real server.
public class OrderApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task GetOrder_ExistingId_Returns200()
{
var client = factory.CreateClient();
var response = await client.GetAsync("/api/orders/1");
response.EnsureSuccessStatusCode();
}
}Q106: What is Testcontainers?
A library that starts real Docker containers (PostgreSQL, Redis, RabbitMQ) from within your tests. Tests run against real infrastructure instead of fakes.
public class DatabaseFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _db = new PostgreSqlBuilder().Build();
public string ConnectionString { get; private set; } = "";
public async Task InitializeAsync()
{
await _db.StartAsync();
ConnectionString = _db.GetConnectionString();
}
public async Task DisposeAsync() => await _db.StopAsync();
}Q107: What is FluentAssertions?
A library that makes test assertions more readable and produces better failure messages.
// xUnit assertion
Assert.Equal(42, result.Id);
Assert.True(result.Items.Count > 0);
// FluentAssertions — reads like English, better error messages
result.Id.Should().Be(42);
result.Items.Should().NotBeEmpty();
result.Status.Should().Be("Pending").Because("newly created orders are always pending");
result.Total.Should().BeGreaterThan(0);Q108: What should you NOT test?
- Framework code (ASP.NET Core routing, EF Core internals)
- Simple property getters/setters with no logic
- Private methods (test them via the public API they support)
- Generated code (migration files, protobuf stubs)
Focus tests on business logic, validation rules, edge cases, and integration of components.
Q109: What is [Theory] with [InlineData]?
[Theory] runs the same test with multiple data sets. [InlineData] provides the values inline.
[Theory]
[InlineData("user@example.com", true)]
[InlineData("notanemail", false)]
[InlineData("", false)]
[InlineData(null, false)]
public void IsValidEmail_VariousInputs(string? input, bool expected)
{
var result = EmailValidator.IsValid(input);
Assert.Equal(expected, result);
}Q110: What is test isolation and why does it matter?
Each test must be independent — it should not depend on the state left by another test, and its outcome should not affect other tests. Without isolation:
- Test order matters → tests pass individually but fail as a suite
- One failure causes cascading failures
- Parallelism causes flaky tests
Strategies:
- Unit tests: create fresh objects in each test (constructor injection, no shared static state)
- Integration tests: rollback transactions or reset the database after each test (Respawn)
- Never share mutable state between test classes
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.