.NET Full Stack Interview Questions for 5 Years Experience
40 questions with answers for candidates with 5 years of experience — 1 easy, 25 medium, 14 hard.
What interviewers expect: 5 years experience
At five years, the focus moves to architecture and performance. Expect questions on scaling APIs, caching strategy with Redis, background processing, message queues, microservices versus modular monolith, memory and thread pool issues, SQL execution plans, deadlocks and safe database migrations. Behavioural rounds cover mentoring, estimation and handling critical incidents. Clear communication with clients also matters.
This set is weighted towards ASP.NET Core Web API (8), SQL Server and T-SQL (6), Entity Framework Core (6), LINQ, Collections and Generics (5).
System design Performance troubleshooting Cloud and Azure Mentoring juniors
Solved at this levelStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
lock ensures that only one thread at a time executes a block of code for a given lock object, preventing race conditions on shared data. It compiles to Monitor.Enter and Monitor.Exit inside try-finally. Lock on a private readonly object, never on this or a string. Since .NET 9 there is a dedicated System.Threading.Lock type that is faster. You cannot await inside a lock; use SemaphoreSlim for async code.
02
What is the difference between temporary tables and table variables?
Temporary tables (#Temp) live in tempdb, support indexes and statistics, and are visible to nested procedures, so they suit larger intermediate results. Table variables (@T) also use tempdb but have limited statistics, which made old optimisers estimate one row; SQL Server 2019 improved this with deferred compilation. Table variables are not affected by transaction rollback. Use table variables for small sets and temp tables for large ones.
An async void method cannot be awaited, so the caller does not know when it finishes. Exceptions thrown inside it cannot be caught by the caller; they go to the synchronization context and can crash the process. It is also hard to unit test. Use async Task instead. The only accepted use of async void is for event handlers, such as button click handlers in desktop apps, where the signature is fixed.
04
What is the difference between IEnumerable and IQueryable?
IEnumerable<T> works on in-memory data and executes LINQ with C# delegates, so filters run in the application. IQueryable<T> holds an expression tree that a provider such as EF Core translates into SQL, so filtering, sorting and paging happen in the database. If you return IEnumerable from a repository and then call Where, all rows may be loaded first. Keep queries as IQueryable until the final shape is ready.
05
What is the difference between a static class and a singleton?
A static class cannot be instantiated, cannot implement interfaces and is initialised by the runtime; it suits stateless helpers like Math. A singleton is a normal class with exactly one instance, so it can implement interfaces, be passed as a parameter and be mocked in tests. In ASP.NET Core, you rarely write a singleton by hand; you register the class with AddSingleton and let the DI container manage its lifetime.
06
Your code builds a LINQ query over a database, then calls Count() and later loops over it, and the logs show two SQL queries. Why, and how do you fix it?
This is multiple enumeration caused by deferred execution. The variable holds a query, not data, so Count runs one SQL statement and the foreach runs another. Besides extra load, the two results may differ if data changes in between. Materialise once with var list = await query.ToListAsync(), then use list.Count and loop over the list. Code analysers flag possible multiple enumeration of IEnumerable as a warning.
Single Responsibility: a class has one reason to change. Open/Closed: extend behaviour without editing existing code. Liskov Substitution: derived types must work wherever the base type is expected. Interface Segregation: prefer small, focused interfaces over one fat interface. Dependency Inversion: depend on abstractions, not concrete classes. In ASP.NET Core, injecting IEmailSender into a controller instead of creating SmtpClient directly applies dependency inversion.
08
How does Dictionary<TKey, TValue> work internally?
Dictionary is a hash table. It calls GetHashCode on the key to choose a bucket, then uses Equals to find the exact entry if several keys share a bucket. Lookups, inserts and deletes are O(1) on average. When it fills up, it resizes and rehashes. Keys must have stable hash codes, so never mutate a key object after inserting it, and override GetHashCode with Equals for custom key types.
09
Tell me about a time you made a mistake that affected data, and what you did.
Early in my job (Situation) I ran an UPDATE on a test copy that I believed was staging, but it was a shared QA database, and 2,000 test orders lost their status. My task was to fix it fast. I told my lead immediately, restored the rows from the nightly backup with the DBA, and verified counts. Result: QA lost only an hour. Since then I always wrap manual updates in a transaction and run a SELECT with the same WHERE first.
10
Give an example of a Liskov Substitution Principle violation and how to fix it.
A classic case is Square inheriting Rectangle. Setting Width on a square also changes Height, so code that sets width 5 and height 4 and expects area 20 breaks. Another real case is a ReadOnlyRepository that inherits Repository and throws NotSupportedException on Save. The fix is to model shared behaviour with interfaces such as IShape or IReadRepository and IWriteRepository, so subclasses never weaken the contract.
ref passes a variable by reference; it must be initialised before the call and the method can read and change it. out also passes by reference but need not be initialised, and the method must assign it before returning, as in int.TryParse. in passes by reference as read-only, which avoids copying large structs while preventing changes. Overusing ref and out makes code harder to read, so tuples are often cleaner.
12
How does garbage collection work in .NET and what are generations?
The GC automatically frees objects that are no longer reachable from roots such as static fields, locals and CPU registers. The heap is split into generations: Gen 0 for new short-lived objects, Gen 1 as a buffer, and Gen 2 for long-lived objects. Most objects die young, so Gen 0 collections are frequent and cheap. Objects over 85,000 bytes go to the Large Object Heap, which is collected with Gen 2.
13
What is the difference between controllers and Minimal APIs?
Controllers group actions into classes with attributes for routing, filters and model binding, which suits large APIs with many conventions. Minimal APIs map endpoints directly with app.MapGet and lambdas, with less ceremony and slightly better performance, ideal for microservices and small APIs. Since .NET 7 they support filters, route groups and OpenAPI, and .NET 10 adds built-in validation, so both are production-ready choices.
14
What is the difference between DELETE, TRUNCATE and DROP?
DELETE removes rows one by one, supports WHERE, fires triggers and is fully logged, so it is slower on big tables. TRUNCATE removes all rows by deallocating pages, is minimally logged, resets identity values and cannot have WHERE; it fails if foreign keys reference the table. DROP removes the whole table including structure, indexes and permissions. In SQL Server, both DELETE and TRUNCATE can be rolled back inside a transaction.
Constraints restrict which types can be used for a type parameter so the generic code can rely on certain members. Examples: where T : class (reference type), where T : struct, where T : new() (has a parameterless constructor), where T : BaseEntity (inherits a class), where T : IComparable<T>, and where T : notnull. A repository might use where T : class, IEntity so it can read the Id property safely.
16
What are services and dependency injection in Angular?
A service is a class, marked @Injectable, that holds logic or data shared across components, such as API calls, authentication or a cart. Angular's DI creates the service and provides it to components that request it in the constructor or with the inject() function. With providedIn: 'root', one shared instance exists for the whole app. This keeps components focused on UI and makes logic easy to test.
17
Write a recursive CTE that lists an employee hierarchy with levels.
WITH Org AS (
SELECT Id, Name, ManagerId, 0 AS Lvl
FROM Employees
WHERE ManagerId IS NULL -- anchor: CEO
UNION ALL
SELECT e.Id, e.Name, e.ManagerId, o.Lvl + 1
FROM Employees e
JOIN Org o ON e.ManagerId = o.Id -- recursive part
)
SELECT REPLICATE(' ', Lvl) + Name AS Hierarchy, Lvl
FROM Org
OPTION (MAXRECURSION 100);
The anchor query runs once and the recursive part repeats until no new rows are found.
18
Add rate limiting to a login endpoint in ASP.NET Core.
Since C# 8, an interface can provide a method body. Classes implementing the interface get that behaviour without writing it, though they can override it. The main use is evolving a published interface: you can add a new member without breaking every existing implementation. The default method is only callable through the interface type, not the class, and overusing it blurs the line with abstract classes.
public class TimingMiddleware(RequestDelegate next, ILogger<TimingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation("{Method} {Path} -> {Status} in {Ms} ms",
context.Request.Method, context.Request.Path,
context.Response.StatusCode, sw.ElapsedMilliseconds);
}
}
// Program.cs
app.UseMiddleware<TimingMiddleware>();
Code before next runs on the request, code after it runs on the response.
22
You must change an API response format, but an older mobile app version still calls it. How do you avoid breaking those users?
Versioning lets you change contracts without breaking existing clients such as mobile apps that users have not updated. Common approaches are URL segments (/api/v1/orders), query strings (?api-version=2.0) and headers. The Asp.Versioning library supports all of them and integrates with OpenAPI docs. URL versioning is the most visible and cache-friendly. Keep old versions running for a published period, then deprecate them with headers.
23
Your multi-tenant SaaS app keeps all tenants in one database. How do you make sure EF Core queries never return another tenant's data?
Add a TenantId column to every tenant-owned entity and a global query filter such as HasQueryFilter(e => e.TenantId == _tenantId), where the tenant ID comes from a scoped provider that reads the authenticated user's claim. Set TenantId automatically in SaveChanges for new rows. Index TenantId, write integration tests that try cross-tenant access, audit every IgnoreQueryFilters call, and consider SQL Server row-level security as a second layer of defence.
24
A read-only product listing endpoint in EF Core uses a lot of memory and CPU. What simple change helps, and why?
By default, EF Core tracks every entity it loads so it can detect changes on SaveChanges, which costs memory and CPU. AsNoTracking tells EF Core not to track the results, making read-only queries faster and lighter. Use it for lists, reports and GET endpoints where you will not update the data. AsNoTrackingWithIdentityResolution avoids duplicate instances of the same entity in results while still skipping tracking.
25
What are Span<T> and Memory<T> and why do they matter for performance?
Span<T> is a ref struct that represents a contiguous slice of memory, such as part of an array, a string or stack memory, without copying it. Slicing a span is free, which makes parsing much faster with zero allocations. Because it is stack-only, it cannot be stored in fields or used across await. Memory<T> is the heap-storable version for async code. ASP.NET Core and System.Text.Json use them heavily.
26
Write an Angular HTTP interceptor that adds a JWT to every API request.
ValueTask<T> is a struct that avoids allocating a Task when a method usually completes synchronously, for example when a value is served from an in-memory cache most of the time. In very hot paths this reduces GC pressure. The rules are stricter: await a ValueTask only once, do not await it concurrently, and do not call .Result before it completes. For normal application code, Task is simpler and recommended.
28
Limit concurrent calls to a third-party API to 5 at a time using SemaphoreSlim.
Never commit them in appsettings.json. During development use the Secret Manager (dotnet user-secrets), which stores them outside the project folder. In production use environment variables or, better, a vault such as Azure Key Vault or AWS Secrets Manager, loaded through a configuration provider and accessed with managed identity so no credential sits in code. Rotate secrets regularly and restrict who can read them.
30
A singleton CacheService injects AppDbContext, and users start seeing stale data and occasional 'a second operation was started on this context' errors. What is wrong?
This is a captive dependency: a scoped DbContext is captured by a singleton, so one context lives forever and is shared by all requests across threads. DbContext is not thread-safe and its change tracker grows stale. Fix it by injecting IServiceScopeFactory or IDbContextFactory<AppDbContext> into the singleton and creating a short-lived context per operation. Enabling ValidateScopes in development catches this at startup.
31
What are ACID properties and the transaction isolation levels in SQL Server?
ACID means Atomicity (all or nothing), Consistency (rules hold), Isolation (transactions do not interfere) and Durability (committed data survives crashes). Isolation levels are READ UNCOMMITTED (dirty reads allowed), READ COMMITTED (the default), REPEATABLE READ, SERIALIZABLE (strictest) and SNAPSHOT, which uses row versions so readers do not block writers. Read Committed Snapshot Isolation is often enabled to reduce blocking in busy OLTP apps.
32
Describe an API you designed end to end and the decisions you made.
In my last project (Situation) we needed a leave-management API for 3,000 employees. My task was the full design. I modelled resources like /employees/{id}/leaves, used DTOs separate from EF entities, JWT with role policies for managers, FluentValidation, ProblemDetails errors and paging on every list. I documented it in OpenAPI and agreed the contract with the Angular team first. Result: front and back ends were built in parallel and launched on time.
33
Show bulk update and delete with ExecuteUpdate and ExecuteDelete.
// mark all unpaid invoices older than 30 days as overdue
int updated = await db.Invoices
.Where(i => !i.IsPaid && i.DueDate < DateTime.UtcNow.AddDays(-30))
.ExecuteUpdateAsync(s => s
.SetProperty(i => i.Status, "Overdue")
.SetProperty(i => i.UpdatedAt, DateTime.UtcNow));
// delete old logs
int deleted = await db.AuditLogs
.Where(l => l.CreatedAt < cutoff)
.ExecuteDeleteAsync();
These run a single SQL UPDATE or DELETE without loading entities, available since EF Core 7. They bypass the change tracker.
34
Explain a technical trade-off you had to justify to a manager or client.
On a reporting module (Situation) the manager wanted everything in EF Core for consistency, but one report was too slow. My task was to recommend a fix without derailing the plan. I measured the EF query against a Dapper version with a hand-tuned SQL query, showed the 8x difference and explained that we would use Dapper only for that read path. Result: the manager approved, the report loaded in under a second, and the rest stayed in EF Core.
35
A query that used to run in 1 second now takes 30 seconds. How do you troubleshoot it?
Capture the actual execution plan and look for table or index scans on large tables, key lookups, big differences between estimated and actual rows, and warnings like implicit conversions or spills to tempdb. Check missing or fragmented indexes and outdated statistics, and update them. Use Query Store to compare the old and new plans; if a plan regressed, you can force the good plan while fixing the root cause.
public sealed class AppConfig
{
private static readonly Lazy<AppConfig> _instance =
new(() => new AppConfig());
public static AppConfig Instance => _instance.Value;
private AppConfig()
{
// load settings once
}
}
Lazy<T> is thread-safe by default and creates the object only on first access. The private constructor and sealed class prevent other instances. In ASP.NET Core, prefer AddSingleton.
37
Several threads update an in-memory Dictionary cache in a singleton service and you see corrupted data and exceptions. What would you use instead?
System.Collections.Concurrent provides thread-safe collections: ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBag and BlockingCollection. They use fine-grained locking or lock-free techniques, so multiple threads can read and write without your own lock. Use ConcurrentDictionary for in-memory caches in singleton services. Note that GetOrAdd may run the value factory more than once under contention, so keep the factory side-effect free or wrap it in Lazy<T>.
38
Configure a one-to-many relationship between Customer and Order using the Fluent API.
public class Customer { public int Id { get; set; } public List<Order> Orders { get; set; } = []; }
public class Order { public int Id { get; set; } public int CustomerId { get; set; } public Customer Customer { get; set; } = null!; }
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
mb.Entity<Order>().Property(o => o.Total).HasPrecision(18, 2);
}
Restrict prevents deleting a customer who still has orders.
39
An API creates a new HttpClient inside a using block for every outgoing call, and under load you see socket exhaustion errors. How do you fix it?
Disposing HttpClient does not immediately free the underlying socket; connections stay in TIME_WAIT, so thousands of short-lived clients exhaust ports. A single static client fixes that but misses DNS changes. The recommended fix is IHttpClientFactory: register builder.Services.AddHttpClient<PaymentClient>() and inject the typed client. The factory pools and rotates handlers, and you can add resilience with Microsoft.Extensions.Http.Resilience for retries and timeouts.
40
Tell me about a time you led a team to deliver a complex feature.
As tech lead (Situation) I owned a vendor-payments module touching Angular, three APIs and SQL Server. My task was to deliver it in six weeks with four developers. I broke it into vertical slices, defined API contracts first, set up feature flags and reviewed every pull request within a day. When one developer struggled with EF Core, I paired with them. Result: we released on time with no critical bugs and two juniors grew into module owners.
Frequently asked questions
What is asked in .NET Full Stack interviews for 5 years of experience?
At five years, the focus moves to architecture and performance. Expect questions on scaling APIs, caching strategy with Redis, background processing, message queues, microservices versus modular monolith, memory and thread pool issues, SQL execution plans, deadlocks and safe database migrations. Behavioural rounds cover mentoring, estimation and handling critical incidents. Clear communication with clients also matters.
How many questions should I practise before a .NET Full Stack interview?
Work through all 40 questions here, then the company-wise sets for the employers you are targeting. Quality matters more than count: be able to explain why each answer is right.
Which topics matter most for 5 years of experience?
Focus first on System design, Performance troubleshooting, Cloud and Azure, Mentoring juniors.