Technical Interview Q&A
Questions and answers pulled from devinterview.io across C#, ASP.NET, EF Core, and LINQ; including the locked ones. Short, direct...keeping it simple.
Questions sourced from devinterview.io. Answers are my own.
C#
What is the difference between a class and a struct?
What it is: Both define custom types, but they have different memory models and behavior.
classis a reference type; stored on the heap; assignment copies the referencestructis a value type; stored on the stack (or inline in the containing object); assignment copies the entire valueclasssupports inheritance;structdoes not (it can implement interfaces)structcannot have a default (parameterless) constructor before C# 10;classcan- Use
structfor small, short-lived, immutable data (e.g.,Point,Color); useclassfor everything else
struct Point { public int X, Y; }
class Person { public string Name; }
var p1 = new Point { X = 1 };
var p2 = p1;
p2.X = 99;
Console.WriteLine(p1.X); // 1 => struct was copied
Note: The rule of thumb is: if it's larger than 16 bytes, has mutable state, or needs to be passed around polymorphically, make it a class.
What is polymorphism in C#?
What it is: Polymorphism lets you call the same method on different types and get different behavior. In C# it comes in two forms.
- Compile-time (static): method overloading; same method name, different signatures
- Runtime (dynamic): method overriding via
virtualandoverride, the correct implementation is chosen at runtime based on the actual type of the object
public class Animal
{
public virtual string Speak() => "...";
}
public class Dog : Animal
{
public override string Speak() => "Woof";
}
public class Cat : Animal
{
public override string Speak() => "Meow";
}
Animal a = new Dog();
Console.WriteLine(a.Speak()); // "Woof" => runtime dispatch
// .
..^____/
`-. ___ )
|| || mh
Note: The key word is
virtual. Without it, the base method is called regardless of the actual type. Withnewinstead ofoverride, you hide the base method but lose polymorphism.
What is the difference between method overloading and method overriding?
What it is: Two different ways to have multiple methods with the same name.
- Overloading: same method name, different parameter list, in the same class. Resolved at compile time.
- Overriding: same method name and signature, in a derived class. The base method must be
virtualorabstract. Resolved at runtime.
// Overloading
int Add(int a, int b) => a + b;
double Add(double a, double b) => a + b;
// Overriding
class Base { public virtual void Print() => Console.WriteLine("Base"); }
class Child : Base { public override void Print() => Console.WriteLine("Child"); }
Note:
newhides a base method without overriding it. If you call through aBasereference, the base version runs. Withoverride, the derived version always runs regardless of the reference type.
What is the difference between Task and Thread?
What it is: Both represent units of work that can run concurrently, but at very different levels of abstraction.
Threadis an OS-level thread; expensive to create and destroy; you manage its lifecycle manuallyTaskis a higher-level abstraction from the TPL; runs on the thread pool by default; supportsasync/await, continuations, cancellation, and composition (WhenAll,WhenAny)Taskdoes NOT necessarily mean a new thread, it can represent any async operation (I/O wait, timer, etc.)- Creating a new
Threadfor I/O is wasteful;Taskwithasync/awaitreleases the thread while waiting
// Thread (low level, rarely needed today)
var thread = new Thread(() => DoWork());
thread.Start();
// Task (preferred)
await Task.Run(() => DoWork());
// Async I/O => no thread blocked at all
var result = await httpClient.GetStringAsync(url);
Note: In modern .NET, you almost never need to create a
Threaddirectly. UseTask,Task.Run, orasync/await. ReserveThreadfor interop with legacy code or when you need specific thread properties.
What is the difference between List and LinkedList?
What it is: Two different data structures that both store ordered sequences but have opposite performance trade-offs.
List<T>: backed by an array; index access is O(1); insert/delete at the middle requires shifting elements: O(n); best for read-heavy or append-only scenariosLinkedList<T>: doubly-linked nodes; insert/delete anywhere is O(1) once you have the node; index access requires traversal: O(n); best when you frequently insert or remove from the middle
var list = new List<int> { 1, 2, 3 };
list.Insert(1, 99); // shifts elements => O(n)
var linked = new LinkedList<int>(new[] { 1, 2, 3 });
var node = linked.Find(2);
linked.AddAfter(node, 99); // O(1) => just pointer updates ...da heck!
Note: In practice
List<T>wins almost every time because CPU cache performance on a contiguous array beats the pointer chasing of a linked list, even for many inserts. Reach forLinkedList<T>only when profiling confirms the bottleneck.
What is the difference between HashTable and Dictionary?
What it is: Both are hash-based key-value stores, but Dictionary<TKey, TValue> is the modern replacement for Hashtable.
Hashtable: non-generic; keys and values areobject; thread-safe for one writer + multiple readers; boxing overheadDictionary<TKey, TValue>: generic; type-safe; no boxing for value types; NOT thread-safe; useConcurrentDictionaryfor thread safetyDictionaryis faster in single-threaded scenarios due to no boxing and better JIT optimization
// Legacy
Hashtable ht = new Hashtable();
ht["key"] = 42; // boxes the int
// Modern
var dict = new Dictionary<string, int>();
dict["key"] = 42; // no boxing
Note:
Hashtableis essentially obsolete in new code. UseDictionary<TKey, TValue>for single-threaded andConcurrentDictionary<TKey, TValue>for multi-threaded scenarios.
LINQ
What is the difference between First and FirstOrDefault?
What it is: Both return the first element that matches a predicate, but they handle an empty result set differently.
First(): throwsInvalidOperationExceptionif no match is foundFirstOrDefault(): returnsdefault(T)(null for reference types, 0 for int, etc.) if no match
var numbers = new[] { 1, 2, 3 };
numbers.First(n => n > 10); // throws InvalidOperationException
numbers.FirstOrDefault(n => n > 10); // returns 0 (default int)
var users = new List<User>();
users.First(); // throws => list is empty
users.FirstOrDefault(); // returns null
Note: The same pattern applies to
Single/SingleOrDefaultandLast/LastOrDefault. TheOrDefaultvariants are the safe option when empty results are expected. With C# 10+ you can provide a fallback:FirstOrDefault(predicate, fallbackValue).
How do you perform pagination with LINQ?
What it is: Skip and Take together implement offset-based pagination.
Skip(n)skips the first n elementsTake(n)takes the next n- Always sort first; without
OrderBy, results are non-deterministic
int page = 2;
int pageSize = 10;
var results = dbContext.Products
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
Note: Offset pagination has a known limitation: it becomes slow on very large datasets (skipping 100,000 rows still scans them). For high-performance scenarios, keyset pagination (WHERE Id > lastSeenId) is more efficient.
What is the role of expression trees in LINQ?
What it is: Expression trees let LINQ providers (like EF Core) inspect your C# lambda and translate it to another language (SQL), rather than just running it as compiled code.
- A
Func<T, bool>is compiled code; it can only be called - An
Expression<Func<T, bool>>is a data structure; it can be read, inspected, and translated - When you call
Where(x => x.Age > 18)onIQueryable<T>, the lambda becomes an expression tree that EF Core converts toWHERE Age > 18 - If you use a
FunconIQueryable, it falls back to loading all records into memory first
// Expression tree => stays as SQL
Expression<Func<User, bool>> expr = u => u.Age > 18;
context.Users.Where(expr); // → WHERE Age > 18
// Delegate => loads everything into memory
Func<User, bool> func = u => u.Age > 18;
context.Users.Where(func); // → SELECT * (then filters in C#)
Note: This is one of the most important conceptual questions in LINQ. The whole power of LINQ providers depends on expression trees. If you can't translate your code, it can't run on the server.
What is IAsyncEnumerable and how does it work with async streams?
What it is: IAsyncEnumerable<T> (C# 8+) lets you stream data asynchronously, yielding each item as it becomes available without buffering the entire result set.
- Iterate with
await foreachinstead of a normalforeach - Useful for: streaming DB results, paginating API responses, processing large files
yield returnworks insideasyncmethods when the return type isIAsyncEnumerable<T>
// Producer
public async IAsyncEnumerable<Product> StreamProductsAsync()
{
await foreach (var batch in FetchBatchesAsync())
foreach (var item in batch)
yield return item;
}
// Consumer
await foreach (var product in StreamProductsAsync())
{
Console.WriteLine(product.Name); // processed one at a time
}
Note: Compare with
Task<List<T>>: that loads everything into memory before returning.IAsyncEnumerable<T>processes each item as it arrives, keeping memory usage flat even for millions of rows.
Entity Framework Core
What are navigation properties?
What it is: Properties on an entity class that reference related entities, letting you traverse relationships in code instead of writing joins.
- Reference navigation: points to a single related entity (
Order.Customer) - Collection navigation: points to a collection of related entities (
Customer.Orders) - EF Core uses them to infer foreign key relationships when building the model
- They drive loading strategies: eager (
Include), lazy (proxy), and explicit (Entry().Collection().Load())
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } // reference nav
public List<OrderItem> Items { get; set; } // collection nav
}
Note: If a navigation property is null when you expect it to be populated, you probably forgot an
Include(). EF Core does not automatically load navigation properties unless lazy loading proxies are configured.
How does cascade delete work in EF Core?
What it is: Cascade delete automatically deletes dependent entities when a principal entity is deleted, rather than leaving orphaned rows.
- Cascade: deleting
Customeralso deletes theirOrders(default for required relationships) - SetNull: FK is set to null when principal is deleted (only works if FK is nullable)
- Restrict / NoAction: prevents deletion if dependents exist; throws
- Configure via Fluent API:
.OnDelete(DeleteBehavior.Cascade)
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.OnDelete(DeleteBehavior.Cascade);
Note: Be careful with cascade delete in many-to-many or multi-level relationships. EF Core can generate circular cascade paths that SQL Server rejects. In those cases, use
Restrictand handle cleanup manually.
How do you seed data in EF Core?
What it is: Database seeding pre-populates tables with initial or reference data. EF Core provides HasData() for code-first seeding via migrations.
- Configured in
OnModelCreatingwithmodelBuilder.Entity<T>().HasData(...) - Seed data is included in migrations and applied with
Update-Database - Keys must be explicit, EF Core needs them to detect changes across migrations
- For environment-specific or large datasets, consider custom seed logic in
Program.csusingcontext.Database.EnsureCreated()or a seeder service
modelBuilder.Entity<Category>().HasData(
new Category { Id = 1, Name = "Electronics" },
new Category { Id = 2, Name = "Books" }
);
Note:
HasDataseeds are meant for static reference data that rarely changes. Changing seeded values generates a new migration. For dynamic or environment-specific data, use a startup seeder class instead.
How does EF Core handle inheritance? (TPH vs TPT)
What it is: When multiple entity types share a base class, EF Core can map the hierarchy to the database in different ways.
- TPH (Table Per Hierarchy): all types in one table; a discriminator column identifies the type. Default in EF Core. Efficient queries but nullable columns for subtype-specific properties.
- TPT (Table Per Type): each type gets its own table; base columns in the base table, subtype columns in subtype tables. Cleaner schema but JOIN required for every query.
- Configure TPT with
[Table("SubTypeName")]or.ToTable("name")on subtype inOnModelCreating
// TPH (default)
public abstract class Animal { public int Id; public string Name; }
public class Dog : Animal { public string Breed; }
public class Cat : Animal { public bool IsIndoor; }
// → one Animals table with Discriminator column
// TPT: add [Table("Dogs")] to Dog, [Table("Cats")] to Cat
Note: TPH is almost always the better default. TPT looks cleaner in the DB but the JOINs add up quickly. Only switch to TPT if you have a specific schema requirement.
ASP.NET Core
What is ASP.NET Core and how does it differ from classic ASP.NET?
What it is: ASP.NET Core is a complete rewrite of ASP.NET. Cross-platform, modular, and built for cloud and microservices from the ground up.
| ASP.NET (classic) | ASP.NET Core | |
|---|---|---|
| Platform | Windows only | Windows, Linux, macOS |
| Hosting | IIS only | Kestrel, IIS, Docker, etc. |
System.Web |
Required | Removed |
| DI | Optional/external | Built-in |
| Performance | Slower | Significantly faster |
| Configuration | web.config (XML) |
JSON, env vars, code |
Note:
System.Webis the biggest break. Everything that depended on it (WebForms, HttpContext as a global static, etc.) was redesigned. If you're asked about migration, the answer is usually "rewrite, not upgrade."
What is the difference between Web API and MVC in ASP.NET Core?
What it is: In ASP.NET Core, the distinction largely collapsed, both use the same controller/routing infrastructure. The difference is in intent and output.
- MVC: returns HTML views rendered server-side via Razor
- Web API: returns data (JSON/XML) consumed by clients (browser JS, mobile, other services)
- Both inherit from
ControllerBase; MVC controllers additionally inherit fromControllerwhich addsView()support [ApiController]attribute opts a controller into Web API conventions (automatic 400 responses, binding inference, no view lookup)
// MVC controller
public class HomeController : Controller // inherits Controller
{
public IActionResult Index() => View(); // returns Razor view
}
// API controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase // ControllerBase only
{
public IActionResult Get() => Ok(products); // returns JSON
}
Note: In ASP.NET Core, there's no longer a separate "Web API" project type. The same project can serve both Razor views and JSON endpoints. The
[ApiController]attribute is what distinguishes API behaviour.
What is OAuth and how is it used in ASP.NET Core?
What it is: OAuth 2.0 is an authorization framework that lets users grant a third-party application limited access to their resources without sharing credentials.
- The app redirects the user to an identity provider (Google, GitHub, Microsoft, etc.)
- The provider authenticates the user and issues an authorization code
- The app exchanges the code for an access token
- The access token is used to access protected resources on behalf of the user
- ASP.NET Core supports it via
AddAuthentication().AddGoogle(...),AddGitHub(...), etc.
builder.Services.AddAuthentication()
.AddGoogle(options => {
options.ClientId = config["Auth:Google:ClientId"];
options.ClientSecret = config["Auth:Google:ClientSecret"];
});
Note: OAuth is for authorization (what can the app access?), not authentication (who is the user?). OpenID Connect (OIDC) is the identity layer built on top of OAuth 2.0 that adds the "who are you?" part. In practice,
AddGoogle()in ASP.NET Core wires up OIDC under the hood.
Questions sourced from devinterview.io. Answers written independently.