C# Interview Questions & Answers
Introduction
This comprehensive guide covers the most common C# and .NET interview questions, organized by topic and difficulty level. Each answer includes explanations and code examples.
Table of Contents
- C# Fundamentals
- Object-Oriented Programming
- Collections and Generics
- LINQ
- Async/Await and Threading
- Memory Management
- .NET Core / .NET 5+
- Entity Framework
- Design Patterns
- System Design
C# Fundamentals
1. What is the difference between value types and reference types?
Answer:
| Aspect | Value Types | Reference Types |
|---|---|---|
| Storage | Stack (usually) | Heap |
| Contains | Actual data | Reference to data |
| Assignment | Copies value | Copies reference |
| Default | 0, false, etc. | null |
| Examples | int, struct, enum | class, interface, delegate |
// Value type - copies the value
int a = 5;
int b = a;
b = 10;
Console.WriteLine(a); // 5 (unchanged)
// Reference type - copies the reference
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine(list1.Count); // 4 (both point to same object)
2. What is boxing and unboxing?
Answer: Boxing converts a value type to object (reference type). Unboxing extracts the value type from the object.
// Boxing: value type β object (heap allocation)
int number = 42;
object boxed = number; // Boxing occurs
// Unboxing: object β value type (requires cast)
int unboxed = (int)boxed; // Unboxing occurs
// Performance impact
for (int i = 0; i < 1000000; i++)
{
object obj = i; // Boxing: 1M heap allocations!
int val = (int)obj; // Unboxing
}
// Avoid with generics
List<int> list = new List<int>(); // No boxing
list.Add(42);
When it happens:
- Passing value type to object parameter
- Storing value type in non-generic collection
- Calling object methods on value types (ToString, GetHashCode on structs)
3. What is the difference between const and readonly?
Answer:
| Feature | const | readonly |
|---|---|---|
| Initialization | Compile time | Runtime (constructor) |
| Implicit | static | Instance or static |
| Types allowed | Primitives, strings | Any type |
| Memory | Compiled into IL | Field in type |
public class Configuration
{
// const: Compile-time constant, value baked into callers
public const double Pi = 3.14159;
// readonly: Runtime constant, can use constructor
public readonly DateTime StartupTime;
public readonly string ConnectionString;
public Configuration(string connStr)
{
StartupTime = DateTime.Now; // Set at runtime
ConnectionString = connStr; // From parameter
}
}
// Static readonly: One value for all instances
public static readonly Guid AppId = Guid.NewGuid();
4. What are nullable value types and nullable reference types?
Answer:
// Nullable value types (since C# 2.0)
int? nullableInt = null;
int? anotherInt = 42;
// Check and access
if (nullableInt.HasValue)
Console.WriteLine(nullableInt.Value);
// Null-coalescing
int result = nullableInt ?? 0;
// Null-conditional
int? length = nullableInt?.ToString().Length;
// Nullable reference types (C# 8.0+, opt-in)
#nullable enable
string nonNullable = "hello"; // Cannot be null
string? nullable = null; // Can be null
// Compiler warnings
// nonNullable = null; // Warning!
Console.WriteLine(nullable.Length); // Warning: possible null
// Null-forgiving operator
Console.WriteLine(nullable!.Length); // "Trust me, it's not null"
5. Explain the ref, out, and in keywords.
Answer:
// ref: Pass by reference, must be initialized before call
void Increment(ref int value)
{
value++;
}
int x = 5;
Increment(ref x); // x is now 6
// out: Pass by reference, must be assigned in method
bool TryParse(string input, out int result)
{
return int.TryParse(input, out result);
}
if (TryParse("42", out int number))
Console.WriteLine(number);
// in: Pass by reference, readonly (no modification allowed)
void PrintLength(in string text)
{
Console.WriteLine(text.Length);
// text = "new"; // Error: cannot modify
}
// ref readonly return (C# 7.2)
ref readonly int GetLargest(in int[] array)
{
int maxIndex = 0;
for (int i = 1; i < array.Length; i++)
if (array[i] > array[maxIndex])
maxIndex = i;
return ref array[maxIndex];
}
Object-Oriented Programming
6. What is the difference between abstract class and interface?
Answer:
| Feature | Abstract Class | Interface |
|---|---|---|
| Inheritance | Single | Multiple |
| Fields | Can have | Cannot (until C# 8) |
| Constructors | Can have | Cannot |
| Access modifiers | Any | Public by default |
| Implementation | Can provide | Default impl (C# 8+) |
| Use case | βis-aβ relationship | βcan-doβ capability |
// Abstract class: shared implementation + contract
public abstract class Animal
{
protected string Name { get; set; }
public Animal(string name) => Name = name;
public abstract void MakeSound(); // Must implement
public virtual void Sleep() // Can override
{
Console.WriteLine($"{Name} is sleeping");
}
}
// Interface: capability contract
public interface ISwimmable
{
void Swim();
// Default implementation (C# 8.0+)
void Float() => Console.WriteLine("Floating...");
}
public class Duck : Animal, ISwimmable
{
public Duck(string name) : base(name) { }
public override void MakeSound() => Console.WriteLine("Quack!");
public void Swim() => Console.WriteLine($"{Name} is swimming");
}
7. Explain the SOLID principles.
Answer:
S - Single Responsibility: A class should have only one reason to change.
// β Bad: Multiple responsibilities
public class User
{
public void Save() { /* database logic */ }
public void SendEmail() { /* email logic */ }
}
// β
Good: Separated responsibilities
public class User { /* user data only */ }
public class UserRepository { public void Save(User user) { } }
public class EmailService { public void SendEmail(User user) { } }
O - Open/Closed: Open for extension, closed for modification.
// β
Good: Extend via inheritance/composition, don't modify existing
public abstract class Shape { public abstract double Area(); }
public class Circle : Shape { public override double Area() => Math.PI * Radius * Radius; }
public class Rectangle : Shape { public override double Area() => Width * Height; }
L - Liskov Substitution: Subtypes must be substitutable for base types.
// β Bad: Square violates Rectangle contract
public class Rectangle { public virtual int Width { get; set; } public virtual int Height { get; set; } }
public class Square : Rectangle
{
public override int Width { set { base.Width = base.Height = value; } }
}
// β
Good: Use composition or separate hierarchy
I - Interface Segregation: Clients shouldnβt depend on interfaces they donβt use.
// β Bad: Fat interface
public interface IWorker { void Work(); void Eat(); void Sleep(); }
// β
Good: Segregated interfaces
public interface IWorkable { void Work(); }
public interface IFeedable { void Eat(); }
D - Dependency Inversion: Depend on abstractions, not concretions.
// β Bad: Direct dependency
public class OrderService
{
private SqlDatabase _database = new SqlDatabase(); // Concrete!
}
// β
Good: Depend on abstraction
public class OrderService
{
private readonly IRepository _repository;
public OrderService(IRepository repository) => _repository = repository;
}
8. What is the difference between method overloading and overriding?
Answer:
public class Calculator
{
// Overloading: Same name, different parameters (compile-time polymorphism)
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
public int Add(int a, int b, int c) => a + b + c;
}
public class Animal
{
// Virtual method - can be overridden
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal
{
// Overriding: Same signature in derived class (runtime polymorphism)
public override void Speak() => Console.WriteLine("Woof!");
}
// new keyword - hides base method (not recommended)
public class Cat : Animal
{
public new void Speak() => Console.WriteLine("Meow!");
}
Animal animal = new Dog();
animal.Speak(); // "Woof!" - override works
Animal cat = new Cat();
cat.Speak(); // "..." - new hides, doesn't override!
Collections and Generics
9. What is the difference between IEnumerable<T> and IQueryable<T>?
Answer:
| Feature | IEnumerable | IQueryable |
|---|---|---|
| Execution | In-memory | Remote (database) |
| Expression | Delegate | Expression tree |
| Evaluation | Client-side | Server-side |
| Use case | LINQ to Objects | LINQ to SQL/EF |
// IEnumerable: All data loaded to memory, filtered in C#
IEnumerable<User> users = dbContext.Users.ToList(); // Loads ALL users
var filtered = users.Where(u => u.Age > 18); // Filters in memory
// IQueryable: Filter translated to SQL, executed on server
IQueryable<User> query = dbContext.Users; // No execution yet
var filtered = query.Where(u => u.Age > 18); // Adds to expression tree
var result = filtered.ToList(); // NOW executes: SELECT * WHERE Age > 18
// Danger: Switching to IEnumerable loses query optimization
var bad = dbContext.Users
.AsEnumerable() // Forces in-memory from here
.Where(u => u.Name.StartsWith("A")); // This runs in C#, not SQL!
10. Explain covariance and contravariance.
Answer:
// Covariance (out): Can use more derived type as less derived
// "Producer" - only outputs T
interface IProducer<out T> { T Produce(); }
IProducer<Dog> dogProducer = new DogFactory();
IProducer<Animal> animalProducer = dogProducer; // β
Dog is Animal
// Contravariance (in): Can use less derived type as more derived
// "Consumer" - only inputs T
interface IConsumer<in T> { void Consume(T item); }
IConsumer<Animal> animalConsumer = new AnimalHandler();
IConsumer<Dog> dogConsumer = animalConsumer; // β
Can handle any Animal
// Built-in examples
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // IEnumerable<out T> is covariant
Action<Animal> feedAnimal = a => a.Eat();
Action<Dog> feedDog = feedAnimal; // Action<in T> is contravariant
LINQ
11. What is deferred execution in LINQ?
Answer: LINQ queries are not executed when defined; they execute when enumerated.
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Query defined but NOT executed
var query = numbers.Where(n => n > 2);
// Modify source data
numbers.Add(6);
// NOW query executes - includes 6!
foreach (var n in query)
Console.WriteLine(n); // 3, 4, 5, 6
// Immediate execution (forces evaluation)
var list = numbers.Where(n => n > 2).ToList(); // ToList()
var count = numbers.Count(n => n > 2); // Aggregation
var first = numbers.First(n => n > 2); // Element access
Why it matters:
- Query can be built incrementally
- Source changes reflected automatically
- Multiple enumeration = multiple executions (can cause performance issues)
12. What is the difference between Select and SelectMany?
Answer:
var departments = new[]
{
new { Name = "IT", Employees = new[] { "Alice", "Bob" } },
new { Name = "HR", Employees = new[] { "Charlie" } }
};
// Select: 1-to-1 mapping (returns nested collection)
var nested = departments.Select(d => d.Employees);
// Result: IEnumerable<string[]> = [["Alice","Bob"], ["Charlie"]]
// SelectMany: 1-to-many mapping (flattens)
var flat = departments.SelectMany(d => d.Employees);
// Result: IEnumerable<string> = ["Alice", "Bob", "Charlie"]
// SelectMany with result selector
var detailed = departments.SelectMany(
d => d.Employees,
(dept, emp) => new { Department = dept.Name, Employee = emp }
);
// Result: [{ IT, Alice }, { IT, Bob }, { HR, Charlie }]
Async/Await and Threading
13. What is the difference between async/await and parallel programming?
Answer:
// Async/Await: Non-blocking I/O, frees thread while waiting
public async Task<string> FetchDataAsync()
{
// Thread returns to pool during await
var data = await httpClient.GetStringAsync(url);
return data;
}
// Parallel: Uses multiple threads for CPU-bound work
public void ProcessInParallel(List<int> numbers)
{
Parallel.ForEach(numbers, n =>
{
// Each item processed on different thread
DoHeavyComputation(n);
});
}
// Task.Run: Offloads CPU work to thread pool
public async Task ProcessAsync()
{
// Runs on thread pool thread
var result = await Task.Run(() => HeavyComputation());
}
| Aspect | Async/Await | Parallel |
|---|---|---|
| Purpose | I/O-bound operations | CPU-bound operations |
| Threads | Frees thread during wait | Uses multiple threads |
| Use case | HTTP calls, DB queries | Data processing, calculations |
14. What happens if you donβt await an async method?
Answer:
// Fire and forget (dangerous!)
public void DoSomething()
{
ProcessAsync(); // No await - method returns immediately
Console.WriteLine("Continues without waiting");
}
private async Task ProcessAsync()
{
await Task.Delay(1000);
throw new Exception("This exception is lost!"); // Unobserved!
}
// Problems:
// 1. Exceptions are swallowed
// 2. No way to know when operation completes
// 3. Application might exit before completion
// Correct approaches:
await ProcessAsync(); // Wait for completion
// Or capture task for later
Task task = ProcessAsync();
// ... do other work ...
await task; // Wait when needed
15. What is ConfigureAwait(false) and when should you use it?
Answer:
// By default, await captures and restores SynchronizationContext
public async Task UpdateUIAsync()
{
var data = await FetchDataAsync(); // Captures UI context
labelStatus.Text = data; // Returns to UI thread β
}
// ConfigureAwait(false) skips context capture
public async Task<string> FetchDataAsync()
{
var response = await httpClient.GetAsync(url)
.ConfigureAwait(false); // Don't need original context
// Continuation runs on any available thread
return await response.Content.ReadAsStringAsync()
.ConfigureAwait(false);
}
Use ConfigureAwait(false) when:
- In library code (no UI context needed)
- In ASP.NET Core (no sync context by default anyway)
- For performance (avoids context switching overhead)
Donβt use when:
- Continuing code needs UI thread access
- In older ASP.NET (not Core) - can cause deadlocks if not careful
Memory Management
16. How does garbage collection work in .NET?
Answer:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MANAGED HEAP β
ββββββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββββββββββ€
β Generation 0 β Generation 1 β Generation 2 β
β (Young) β (Survivor) β (Long-lived) β
β β β β
β New objects β Survived 1 GC β Survived 2+ GCs β
β Collected β Collected less β Collected rarely β
β frequently β frequently β (Full GC) β
ββββββββββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββββββββββ
GC Process:
- Mark: Find all reachable objects from roots (stack, static fields)
- Sweep: Reclaim memory from unmarked objects
- Compact: Move surviving objects together, update references
// Force garbage collection (rarely needed)
GC.Collect();
GC.WaitForPendingFinalizers();
// Check generation
var obj = new object();
Console.WriteLine(GC.GetGeneration(obj)); // 0
// Large Object Heap (objects > 85KB)
var largeArray = new byte[100000]; // Goes to LOH
// LOH is only collected during Gen 2 collection
17. What is the IDisposable pattern and why is it important?
Answer:
public class ResourceHolder : IDisposable
{
private FileStream _fileStream;
private bool _disposed = false;
public ResourceHolder(string path)
{
_fileStream = new FileStream(path, FileMode.Open);
}
// Public Dispose method
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // No need for finalizer
}
// Protected virtual for inheritance
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Dispose managed resources
_fileStream?.Dispose();
}
// Free unmanaged resources here
_disposed = true;
}
// Finalizer (only if you have unmanaged resources)
~ResourceHolder()
{
Dispose(false);
}
}
// Usage with using statement
using (var holder = new ResourceHolder("file.txt"))
{
// Use the resource
} // Dispose called automatically
// C# 8+ using declaration
using var holder = new ResourceHolder("file.txt");
// Disposed at end of scope
.NET Core / .NET 5+
18. What is dependency injection and how does it work in .NET?
Answer:
// 1. Define services
public interface IEmailService
{
Task SendAsync(string to, string message);
}
public class SmtpEmailService : IEmailService
{
public async Task SendAsync(string to, string message)
{
// Send via SMTP
}
}
// 2. Register in DI container
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddSingleton<IConfiguration>(config);
builder.Services.AddTransient<IValidator, OrderValidator>();
// 3. Inject via constructor
public class OrderController : ControllerBase
{
private readonly IEmailService _emailService;
public OrderController(IEmailService emailService)
{
_emailService = emailService; // Injected automatically
}
}
Service Lifetimes:
| Lifetime | Description | Use Case |
|---|---|---|
| Transient | New instance each request | Stateless services |
| Scoped | One instance per HTTP request | DbContext, unit of work |
| Singleton | One instance for app lifetime | Configuration, caching |
19. What is middleware in ASP.NET Core?
Answer:
// Middleware pipeline - request flows through each component
public void Configure(IApplicationBuilder app)
{
// Order matters!
app.UseExceptionHandler("/error"); // 1. Handle exceptions
app.UseHttpsRedirection(); // 2. Redirect HTTP to HTTPS
app.UseStaticFiles(); // 3. Serve static files
app.UseRouting(); // 4. Match routes
app.UseAuthentication(); // 5. Authenticate user
app.UseAuthorization(); // 6. Check permissions
app.UseEndpoints(endpoints => { }); // 7. Execute endpoint
}
// Custom middleware
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
public RequestTimingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
await _next(context); // Call next middleware
stopwatch.Stop();
context.Response.Headers.Add("X-Response-Time",
$"{stopwatch.ElapsedMilliseconds}ms");
}
}
// Register: app.UseMiddleware<RequestTimingMiddleware>();
Entity Framework
20. What is the difference between Include and ThenInclude?
Answer:
// Include: Load direct navigation property
var orders = await context.Orders
.Include(o => o.Customer) // Load Customer for each Order
.ToListAsync();
// ThenInclude: Load nested navigation properties
var orders = await context.Orders
.Include(o => o.OrderItems) // Load OrderItems
.ThenInclude(oi => oi.Product) // Load Product for each OrderItem
.ThenInclude(p => p.Category) // Load Category for each Product
.Include(o => o.Customer) // Also load Customer
.ThenInclude(c => c.Address) // Load Address for Customer
.ToListAsync();
// Explicit loading (after the fact)
var order = await context.Orders.FindAsync(1);
await context.Entry(order)
.Collection(o => o.OrderItems)
.LoadAsync();
21. How do you handle concurrency in Entity Framework?
Answer:
// Optimistic concurrency with RowVersion
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; } // Concurrency token
}
// OnModelCreating alternative
modelBuilder.Entity<Product>()
.Property(p => p.RowVersion)
.IsRowVersion();
// Handling concurrency conflicts
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
var clientValues = entry.CurrentValues;
// Strategy 1: Database wins
entry.OriginalValues.SetValues(databaseValues);
// Strategy 2: Client wins
entry.OriginalValues.SetValues(databaseValues);
// Keep current values, retry save
// Strategy 3: Merge (custom logic)
// Compare and merge values
await context.SaveChangesAsync();
}
Design Patterns
22. Explain the Repository and Unit of Work patterns.
Answer:
// Repository: Abstracts data access
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
}
// Unit of Work: Coordinates repositories, single transaction
public interface IUnitOfWork : IDisposable
{
IRepository<Order> Orders { get; }
IRepository<Product> Products { get; }
Task<int> SaveChangesAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public IRepository<Order> Orders { get; }
public IRepository<Product> Products { get; }
public UnitOfWork(AppDbContext context)
{
_context = context;
Orders = new Repository<Order>(context);
Products = new Repository<Product>(context);
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public void Dispose() => _context.Dispose();
}
// Usage
public class OrderService
{
private readonly IUnitOfWork _uow;
public async Task CreateOrderAsync(Order order)
{
await _uow.Orders.AddAsync(order);
var product = await _uow.Products.GetByIdAsync(order.ProductId);
product.Stock -= order.Quantity;
_uow.Products.Update(product);
await _uow.SaveChangesAsync(); // Single transaction
}
}
System Design
23. How would you design a rate limiter?
Answer:
// Token Bucket Algorithm implementation
public class TokenBucketRateLimiter
{
private readonly int _maxTokens;
private readonly double _refillRate; // Tokens per second
private double _tokens;
private DateTime _lastRefill;
private readonly object _lock = new();
public TokenBucketRateLimiter(int maxTokens, double refillRate)
{
_maxTokens = maxTokens;
_refillRate = refillRate;
_tokens = maxTokens;
_lastRefill = DateTime.UtcNow;
}
public bool TryAcquire()
{
lock (_lock)
{
RefillTokens();
if (_tokens >= 1)
{
_tokens--;
return true;
}
return false;
}
}
private void RefillTokens()
{
var now = DateTime.UtcNow;
var elapsed = (now - _lastRefill).TotalSeconds;
var tokensToAdd = elapsed * _refillRate;
_tokens = Math.Min(_maxTokens, _tokens + tokensToAdd);
_lastRefill = now;
}
}
// ASP.NET Core built-in rate limiting (.NET 7+)
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
options.AddSlidingWindowLimiter("sliding", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 4;
});
});
24. How would you implement caching in a .NET application?
Answer:
// In-memory caching
public class CachedProductService : IProductService
{
private readonly IMemoryCache _cache;
private readonly IProductRepository _repository;
public async Task<Product> GetByIdAsync(int id)
{
var cacheKey = $"product_{id}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(10);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
return await _repository.GetByIdAsync(id);
});
}
public void InvalidateProduct(int id)
{
_cache.Remove($"product_{id}");
}
}
// Distributed caching with Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApp_";
});
public class RedisProductService
{
private readonly IDistributedCache _cache;
public async Task<Product> GetByIdAsync(int id)
{
var cacheKey = $"product_{id}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
return JsonSerializer.Deserialize<Product>(cached);
var product = await _repository.GetByIdAsync(id);
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(10)
});
return product;
}
}
Quick Reference
Common Interview Topics Checklist
- [ ] Value types vs Reference types
- [ ] Boxing/Unboxing
- [ ] Abstract class vs Interface
- [ ] SOLID principles
- [ ] IEnumerable vs IQueryable
- [ ] Async/Await mechanics
- [ ] Garbage collection
- [ ] Dependency injection
- [ ] Entity Framework (Include, tracking, concurrency)
- [ ] Design patterns (Repository, Factory, Singleton)
- [ ] Middleware pipeline
- [ ] Exception handling best practices
- [ ] Caching strategies
- [ ] API versioning
- [ ] Authentication/Authorization