This guide covers the questions interviewers actually ask — from deep C# internals and async patterns to ASP.NET Core, Entity Framework, microservices, design patterns, and system design. Each answer goes beyond the surface level so you can confidently discuss trade-offs.
🔷 Section 1 — C# Core & Language Internals
1What is the difference betweenstructandclassin C#? When would you choose one over the other?
A class is a reference type allocated on the heap; a struct is a value type typically allocated on the stack (or inline in its containing type). The key differences are:
| Aspect |
Class |
Struct |
| Memory |
Heap |
Stack (usually) |
| Inheritance |
Supported |
Not supported (can implement interfaces) |
| Nullability |
Can be null |
Cannot be null (unless Nullable<T>) |
| Copy behaviour |
Reference copied |
Entire value copied |
| Default ctor |
Provided if none defined |
Implicit parameterless ctor in C# 10+ |
Choose struct when: the type is small (≤16 bytes), logically represents a single value, is immutable, and doesn't need to be boxed frequently. Examples: Point, DateTime, Guid.
Senior angle: Misusing structs with large fields or frequent boxing (e.g., storing in IList<object>) can hurt performance more than using a class.
2Explainasync/awaitunder the hood. What is a continuation, and how does the state machine work?
When the compiler processes an async method, it transforms it into a state machine — a struct implementing IAsyncStateMachine. Each await point becomes a state transition.
public async Task<string> FetchDataAsync(string url)
{
var client = new HttpClient();
// Compiler inserts a suspension point here
var result = await client.GetStringAsync(url);
return result.ToUpper();
}
When await is hit: if the awaitable is already complete, execution continues synchronously. If not, a continuation (callback) is registered on the Task, and the method returns the incomplete Task to the caller. When the awaited work finishes, the continuation resumes the state machine at the next state.
Common pitfall: Not using ConfigureAwait(false) in library code can cause deadlocks in synchronisation-context environments (e.g., old ASP.NET or WinForms).
3What are the differences betweenIEnumerable<T>,IQueryable<T>, andICollection<T>?
| Interface |
Execution |
Use Case |
IEnumerable<T> |
In-memory (deferred) |
LINQ-to-Objects, streaming sequences |
IQueryable<T> |
Deferred, translated to SQL |
EF Core queries — filter at DB level |
ICollection<T> |
In-memory, materialised |
When you need Count and Add/Remove |
Critical distinction: An IQueryable builds an expression tree. Only when you materialise it (e.g., .ToList()) does the SQL get generated and sent to the database. Calling LINQ methods on an already-materialised IEnumerable (after ToList()) performs the filtering in memory — always be deliberate about where you materialise.
4What is the purpose ofSpan<T>andMemory<T>? Give a real-world scenario.
Span<T> is a stack-only, ref struct that provides a safe, zero-copy window into a contiguous block of memory (array, stack-allocated buffer, or unmanaged memory). Memory<T> is the heap-compatible counterpart that can be stored in fields and used with async.
// Parse a CSV line without allocating substrings
void ParseLine(ReadOnlySpan<char> line)
{
int comma = line.IndexOf(',');
ReadOnlySpan<char> first = line[..comma];
ReadOnlySpan<char> second = line[(comma + 1)..];
// No heap allocation for substrings
}
Real-world use: High-throughput JSON/CSV parsing, network buffer slicing in ASP.NET Core Kestrel, and any hot path where reducing GC pressure is critical.
5Explain the difference between==andEquals()for reference vs value types. What aboutReferenceEquals()?
== (operator): For reference types, checks reference equality by default (unless overloaded, e.g., string). For value types, checks value equality.
Equals() (virtual method): Can be overridden to provide semantic/value equality. string.Equals() compares content, not reference.
ReferenceEquals(): Always compares object identity (pointer equality). Cannot be overridden. Even two interned strings with the same content may return true from ReferenceEquals due to string interning.
Best practice: When implementing value equality on a class, override both Equals() and GetHashCode(), and consider implementing IEquatable<T> to avoid boxing.
🔷 Section 2 — ASP.NET Core
6Explain the ASP.NET Core middleware pipeline. How does it differ from HTTP Modules/Handlers in classic ASP.NET?
In ASP.NET Core, the middleware pipeline is a chain of request delegates. Each middleware component receives an HttpContext, can execute code before and after the next component, and decides whether to call next().
app.Use(async (context, next) =>
{
// Before next middleware
await next.Invoke();
// After next middleware (response phase)
});
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
vs. HTTP Modules (classic ASP.NET): Modules were event-based and tightly coupled to System.Web. They fired on specific pipeline events (BeginRequest, AuthenticateRequest, etc.) in a fixed order. Middleware is simpler, more composable, and completely decoupled from IIS. Order is explicit and determined by the order you register in Program.cs.
7What are the DI lifetimes in ASP.NET Core and what problems can arise from choosing the wrong one?
| Lifetime |
Created |
Example Use |
| Transient |
Every injection |
Lightweight stateless services |
| Scoped |
Once per HTTP request |
DbContext, unit-of-work |
| Singleton |
Once for app lifetime |
Caching, configuration, HttpClient factory |
Captive dependency problem: Injecting a Scoped or Transient service into a Singleton causes the shorter-lived service to be "captured" and reused for the entire app lifetime — causing stale data, threading issues, or leaked DbContext connections.
Detection: ASP.NET Core throws a InvalidOperationException at startup if scope validation is enabled (default in Development). Enable it explicitly in Production with ValidateScopes = true.
8How does minimal API differ from controller-based API in ASP.NET Core? What are the trade-offs?
Minimal APIs (introduced in .NET 6) define endpoints directly in Program.cs without controllers or action methods:
app.MapGet("/products/{id}", async (int id, IProductService svc) =>
await svc.GetByIdAsync(id) is { } product
? Results.Ok(product)
: Results.NotFound());
| |
Minimal API |
Controller-based |
| Boilerplate |
Minimal |
More structured |
| Filters |
Limited (endpoint filters) |
Full action/result filter pipeline |
| Organisation |
Can get messy at scale |
Better for large APIs |
| Performance |
Slightly faster startup |
Minimal difference at runtime |
When to use Minimal APIs: Microservices, small focused APIs, or when you want maximum control with minimum overhead. Use controllers for larger projects with complex cross-cutting concerns.
🔷 Section 3 — Entity Framework Core
9What is the N+1 problem in EF Core, and how do you resolve it?
The N+1 problem occurs when you load a collection and then lazily fetch related data for each item — resulting in 1 query for the list + N queries for the related entities.
// ❌ N+1: 1 query for orders + N queries for customers
var orders = context.Orders.ToList();
foreach (var o in orders)
Console.WriteLine(o.Customer.Name); // lazy load per order
// ✅ Eager loading — single JOIN query
var orders = context.Orders
.Include(o => o.Customer)
.ToList();
Solutions:
• Eager loading with Include() / ThenInclude() — join in a single query.
• Explicit loading with context.Entry(entity).Collection().LoadAsync() — when conditional loading is needed.
• Projection — use Select() to fetch only the columns you need, avoiding full entity hydration.
Disable lazy loading in EF Core by default (UseLazyLoadingProxies() must be opted into). This prevents accidental N+1 issues.
10What is the difference betweenAsNoTracking()and tracked queries in EF Core?
By default, EF Core tracks entities returned from queries using a change tracker. This allows it to detect modifications and generate appropriate UPDATE/DELETE SQL when you call SaveChanges().
AsNoTracking() skips the change tracker entirely. The entities are still materialised but EF Core doesn't monitor them. This is faster and uses less memory — ideal for read-only scenarios (report endpoints, read APIs).
// Read-only — no tracking overhead
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
Use tracking when you need to update entities after loading. Use AsNoTracking() when you only read data — it can be 20–30% faster for large result sets.
11How do you handle database migrations in a team environment with EF Core?
Managing migrations in a team requires discipline around:
1. Never auto-apply in Production. Use Database.Migrate() only in Development or CI pipelines. In Production, generate SQL scripts and apply them via your deployment process.
# Generate idempotent SQL script
dotnet ef migrations script --idempotent -o migration.sql
2. Merge conflicts. When two developers add migrations simultaneously, one must remove their migration, pull the other's, and re-apply on top. Never edit a migration that's been applied to Production.
3. Squash old migrations. For large projects, periodically squash old migrations into a baseline snapshot to reduce startup time and history noise.
4. Separate migration projects for large solutions to keep data access concerns isolated.
🔷 Section 4 — Design Patterns & Architecture
12Explain the Repository and Unit of Work patterns. When should you NOT use them?
The Repository pattern abstracts data access behind an interface, decoupling business logic from the ORM. The Unit of Work groups multiple repository operations into a single transaction.
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task AddAsync(Product product);
}
public interface IUnitOfWork
{
IProductRepository Products { get; }
Task<int> SaveChangesAsync();
}
When NOT to use it: EF Core's DbContext already IS a Unit of Work, and DbSet<T> is already a Repository. Adding a generic wrapper on top (e.g., Repository<T> that just delegates to DbSet) adds boilerplate with zero benefit. Use it only when you need to abstract across multiple data sources or need testability without mocking EF Core internals.
13What is CQRS and how would you implement it in a .NET application?
CQRS (Command Query Responsibility Segregation) separates read (query) and write (command) models. Commands change state; queries return data. This allows independent scaling and optimisation of each side.
A popular .NET implementation uses MediatR:
// Command
public record CreateOrderCommand(int CustomerId, List<OrderItem> Items)
: IRequest<Guid>;
// Handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(
CreateOrderCommand cmd, CancellationToken ct)
{
var order = new Order(cmd.CustomerId, cmd.Items);
await _repo.AddAsync(order, ct);
return order.Id;
}
}
Advanced: In event-driven CQRS, the write side publishes domain events; the read side subscribes and builds denormalised read models (projections) into a separate read store (e.g., Elasticsearch, Redis).
14What is the Decorator pattern and how does ASP.NET Core's DI support it?
The Decorator pattern wraps a service to add behaviour (caching, logging, validation) without modifying the original class — following the Open/Closed Principle.
// Original service
public class ProductService : IProductService { ... }
// Decorator adds caching
public class CachedProductService : IProductService
{
private readonly IProductService _inner;
private readonly IMemoryCache _cache;
public async Task<Product?> GetByIdAsync(int id) =>
await _cache.GetOrCreateAsync($"product-{id}",
_ => _inner.GetByIdAsync(id));
}
// Registration (manual decorator wiring)
services.AddScoped<ProductService>();
services.AddScoped<IProductService>(sp =>
new CachedProductService(sp.GetRequiredService<ProductService>(),
sp.GetRequiredService<IMemoryCache>()));
Libraries like Scrutor provide .Decorate<IProductService, CachedProductService>() for cleaner registration.
🔷 Section 5 — Performance & Memory Management
15How does the .NET Garbage Collector work? What are Gen0, Gen1, and Gen2?
The .NET GC uses a generational model based on the assumption that most objects die young:
| Generation |
Description |
Collection frequency |
| Gen0 |
Newly allocated small objects |
Very frequent (milliseconds) |
| Gen1 |
Objects that survived Gen0 |
Moderate |
| Gen2 |
Long-lived objects (singletons, caches) |
Infrequent (seconds to minutes) |
| LOH |
Large Object Heap (≥85KB) |
With Gen2; not compacted by default |
Objects that survive a collection are promoted to the next generation. Gen2 collections are the most expensive (stop-the-world in some modes). In server GC mode, each processor core has its own heap to enable parallel collection.
Perf tip: Avoid creating large, short-lived objects on the LOH (e.g., large byte arrays in tight loops). Use ArrayPool<T>.Shared.Rent() to reuse buffers and reduce LOH pressure.
16What isIDisposableand the Dispose pattern? When should you useusingvs. DI lifetime management?
Implement IDisposable when your class holds unmanaged resources (file handles, DB connections, native memory) or references to other IDisposable objects. The full dispose pattern for classes that may be subclassed:
public class ResourceWrapper : IDisposable
{
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Free managed resources
_stream?.Dispose();
}
// Free unmanaged resources here
_disposed = true;
}
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
}
Use using for locally-scoped disposables (streams, connections). Let DI manage lifetime for services registered as Scoped/Singleton — the DI container calls Dispose() when the scope ends.
🔷 Section 6 — Microservices & Distributed Systems
17What is the difference between synchronous (REST/gRPC) and asynchronous (message queues) inter-service communication? When do you choose each?
| Aspect |
Synchronous (REST/gRPC) |
Asynchronous (RabbitMQ, Azure Service Bus) |
| Coupling |
Temporal coupling |
Decoupled in time |
| Failure handling |
Caller waits; upstream failure propagates |
Message persisted; consumer can retry |
| Latency |
Low for simple request-response |
Higher (broker round-trip) |
| Use case |
Queries needing immediate response |
Commands, events, long-running workflows |
Choose synchronous when: the caller needs an immediate result (user-facing query, auth validation). Choose async when: order placed → email sent → inventory updated — these can happen independently and should be resilient to partial failure.
Pattern: The Outbox pattern ensures reliable event publishing: write the event to the same DB transaction as the domain change, then a background job publishes it to the broker.
18How do you implement resilience in a .NET microservice? What is Polly?
Polly is a .NET resilience library that provides policies for: Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback.
// .NET 8 — Resilience pipeline via Microsoft.Extensions.Resilience
services.AddHttpClient<IOrderClient, OrderClient>()
.AddResilienceHandler("orders", builder =>
{
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential
});
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = TimeSpan.FromSeconds(10),
FailureRatio = 0.5,
MinimumThroughput = 10
});
builder.AddTimeout(TimeSpan.FromSeconds(5));
});
Circuit Breaker states: Closed (normal) → Open (failing; fast-fail requests) → Half-Open (test recovery) → Closed.
🔷 Section 7 — Security
19How does JWT authentication work in ASP.NET Core, and what are common security pitfalls?
A JWT (JSON Web Token) consists of three base64-encoded parts: Header (algorithm), Payload (claims), and Signature. The server signs it with a secret (HS256) or private key (RS256). Clients send it in the Authorization: Bearer <token> header.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
ClockSkew = TimeSpan.Zero // Remove 5-min leeway
};
});
Common pitfalls:
• Storing JWTs in localStorage (XSS vulnerable) — prefer HttpOnly cookies.
• Not validating alg — an attacker could send "alg": "none".
• Long-lived access tokens — use short expiry (15 min) with refresh tokens.
• Not checking token revocation — JWTs are stateless; use a deny-list or short expiry.
20What is the difference between Authentication and Authorisation in ASP.NET Core?
Authentication answers "Who are you?" — verifying identity (JWT, cookies, API keys). Authorization answers "What are you allowed to do?" — verifying permissions.
// Policy-based authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("MinAge18", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
// On controller/endpoint
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminDashboard() => Ok();
ASP.NET Core supports role-based, claim-based, and policy-based authorisation. Always prefer policy-based as it is more expressive and testable.
🔷 Section 8 — Testing
21What is the difference between unit, integration, and end-to-end tests? How do you structure them in a .NET project?
| Type |
Scope |
Speed |
.NET Tools |
| Unit |
Single class/method, mocked dependencies |
Milliseconds |
xUnit, NUnit, Moq, NSubstitute |
| Integration |
Multiple components + real DB / HTTP |
Seconds |
xUnit + WebApplicationFactory, Testcontainers |
| E2E |
Full stack — UI to DB |
Minutes |
Playwright, Selenium |
Recommended structure: Follow the Testing Trophy — heavy integration tests in the middle, a solid layer of unit tests, minimal E2E tests. Separate test projects per type: MyApp.UnitTests, MyApp.IntegrationTests.
Integration testing tip: Use WebApplicationFactory<Program> to spin up the full ASP.NET Core pipeline in-process without a real server, combined with Testcontainers for a real database in Docker.
22How do you mock dependencies in unit tests? Explain the difference between mocks, stubs, and fakes.
Stub: Returns canned responses; used to isolate the system under test. No assertion on calls.
Mock: A stub that also asserts it was called correctly. Created with Moq or NSubstitute.
Fake: A working lightweight implementation (e.g., in-memory database) used instead of the real one.
// Moq example — mock + verify
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(new Product { Id = 1, Name = "Widget" });
var service = new ProductService(mockRepo.Object);
var result = await service.GetProductAsync(1);
mockRepo.Verify(r => r.GetByIdAsync(1), Times.Once);
Assert.Equal("Widget", result.Name);
🔷 Section 9 — .NET Runtime & Advanced Topics
23What is the difference between .NET Framework, .NET Core, and .NET 5+? Why did Microsoft unify them?
| Platform |
Cross-platform? |
Open Source? |
Status |
| .NET Framework 4.x |
Windows only |
Partial |
Maintenance mode |
| .NET Core 1–3.x |
Yes |
Yes |
Succeeded by .NET 5+ |
| .NET 5 / 6 / 7 / 8 / 9 |
Yes |
Yes |
Current/LTS |
Microsoft unified the fragmented ecosystem under a single "just .NET" brand starting with .NET 5 in 2020. It combines the performance and cross-platform benefits of .NET Core with modern C# features. LTS releases (6, 8) are supported for 3 years.
24What is theIHostedService/BackgroundServicepattern in .NET?
BackgroundService is an abstract base class implementing IHostedService for running long-running background tasks within an ASP.NET Core application — like consuming a message queue, running scheduled jobs, or processing a channel.
public class OrderProcessorService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var order in _channel.ReadAllAsync(stoppingToken))
{
await ProcessOrderAsync(order);
}
}
}
// Register in Program.cs
builder.Services.AddHostedService<OrderProcessorService>();
The hosted service starts with the app and is gracefully stopped when the app shuts down via the CancellationToken. Use Channel<T> for producer-consumer patterns within a single process.
25What is the difference betweenTask.WhenAllandTask.WhenAny? When would you useParallel.ForEachAsync?
Task.WhenAll: Awaits all tasks to complete. If any task faults, the exception is aggregated. Use for fan-out parallel async work where all results are needed.
Task.WhenAny: Returns as soon as the first task completes. Use for timeout patterns or "first result wins" scenarios (e.g., querying multiple cache layers).
// WhenAll — fetch user + orders + products in parallel
var (user, orders) = await (Task.WhenAll(GetUserAsync(id), GetOrdersAsync(id)))
.ContinueWith(t => (t.Result[0], t.Result[1]));
// Parallel.ForEachAsync — bounded concurrency over a large collection
await Parallel.ForEachAsync(productIds,
new ParallelOptions { MaxDegreeOfParallelism = 4 },
async (id, ct) => await ProcessProductAsync(id, ct));
Parallel.ForEachAsync (introduced in .NET 6) is ideal when you have a large collection and want bounded concurrency — better than spawning unlimited tasks with Task.WhenAll on a huge list.