.NET Full Stack Developer — Interview Questions
In short: Use the skeleton: business context → frontend (framework, state, auth) → API (layers, validation, errors, logging) → data (EF/Dapper, SQL Server, stored procedures) → security (identity provider, JWT, roles) → delivery (Git, CI/CD, hosting) → your part and one measurable result.
- Draw the boxes while you speak and keep it to about two minutes; the interviewer will drill into what interests them.
In short: Form validation → service calls POST /api/... with JSON → interceptor adds the JWT → browser sends CORS preflight if cross-origin → Kestrel → middleware (exception handling, routing, CORS, authentication, authorization) → controller/endpoint → model binding and validation (400 if invalid) → service applies business rules → EF Core tracks the new entity → SaveChangesAsync opens a transaction and runs INSERT → SQL Server checks constraints and commits → API returns 201 Created with the new resource → UI updates the list and shows a message.
- Errors flow back as ProblemDetails and are shown by the interceptor.
In short: Authentication proves who the caller is (token, cookie); failure → 401 Unauthorized.
- Authorization decides what an authenticated caller may do (roles, policies, claims); failure → 403 Forbidden.
- In ASP.NET Core:
UseAuthentication()thenUseAuthorization(),[Authorize(Roles = "Admin")]or policies.
In short: CORS (Cross-Origin Resource Sharing) is a browser protection: JavaScript from origin A (e.g. http://localhost:4200) may read a response from origin B (https://api.example.com) only if B sends Access-Control-Allow-Origin allowing A.
- Non-simple requests trigger an
OPTIONSpreflight. - Postman and server-to-server calls don't enforce it.
- Fix:
builder.Services.AddCors(...)with the exact SPA origins andapp.UseCors(...)before auth — neverAllowAnyOrigintogether with credentials.
In short: Entities expose internal fields (passwords, audit columns), create circular references for JSON, couple the API contract to the database schema, and with lazy loading can trigger extra queries during serialisation.
- DTOs give a stable contract, only the needed fields, and let you project in SQL (
Select(o => new OrderDto(...))) so less data is read.
In short: Server-side: the API takes page, size (clamped to a maximum, e.g. 100), sort and q;
- EF Core builds
Where→OrderBy→Skip/Take, returns{ items, total }(or a continuation token for keyset paging on very large tables). - SQL needs an index that supports the filter + order.
- UI: debounce the search box (300 ms), cancel the previous request (
switchMapin RxJS /AbortController), keep page/sort in the URL query string so links and Back work.
In short: Safest: don't expose tokens to JavaScript — use a Backend-for-Frontend that keeps tokens server-side and gives the browser an HttpOnly, Secure, SameSite cookie.
- If the SPA holds tokens, keep the short-lived access token in memory (not
localStorage, which XSS can read) and use refresh-token rotation via the identity provider (MSAL / oidc-client handle this). - On a 401 the interceptor refreshes once and retries; if refresh fails, redirect to login.
In short: API: AddProblemDetails() + an IExceptionHandler (or exception middleware) maps known exceptions to 400/404/409 and logs unknown ones as 500 with a correlation id;
[ApiController]returns validation errors automatically.- UI: one HTTP interceptor (Angular) or fetch wrapper (React) maps status codes to field errors, a login redirect, a "no permission" page, or a toast showing the correlation id;
- React error boundaries / Angular
ErrorHandlercatch rendering errors.
In short: One query loads N parents, then one more query runs per parent to load children (often through lazy loading or a query inside a loop) — 1 + N round trips.
- Detect it with EF Core logging / Application Insights dependency telemetry / SQL Profiler showing many identical queries.
- Fix: eager load with
Include, or better project only what you need in one query (Select), useAsSplitQueryfor large graphs, and never query inside a loop.
In short: Inject HttpClient into a service that returns an Observable<T>; in the component either subscribe with takeUntilDestroyed() or use the async pipe / toSignal.
- Keep
loading,erroranddatastate (signals or a store), set loading before the call, clear it infinalize, catch errors withcatchError. - For search, use
debounceTime+distinctUntilChanged+switchMapso older requests are cancelled.
In short: HTTPS only (HSTS);
- authentication with JWT bearer from a trusted identity provider;
- authorization with policies and resource checks (user may only see their own orders — prevents IDOR);
- input validation;
- parameterised queries (EF/Dapper do this — never concatenate SQL);
- rate limiting (
AddRateLimiter); - CORS restricted to known origins;
- secrets in Key Vault;
- security headers;
- no stack traces to clients;
- dependency scanning in CI;
- logging without sensitive data.
In short: Domain, Application, Infrastructure and Api projects (clean architecture), tests projects alongside, and the SPA in its own folder.
- Dependency rule inwards; DI registers Infrastructure implementations of Application interfaces.
- Cross-cutting: global error handling with ProblemDetails, validation (FluentValidation), structured logging with correlation ids, health checks, OpenAPI.
- Start as a modular monolith, one database with schemas per module.
- Add CI from day one.
In short: Measure layer by layer: browser DevTools Network (is it the API call or rendering?), API timing / Application Insights (controller vs SQL time), EF Core logs or SQL Profiler for the generated SQL, actual execution plan in SSMS.
- Typical fixes: server-side paging and projection, a covering index for the filter/sort columns, removing N+1 queries,
AsNoTracking, caching reference data, removing.Resultblocking, virtualising long lists and lazy-loading the route in the UI. - Re-measure after each change and report the before/after numbers.
In short: Usually a modular monolith first: one deployable, clear module boundaries, one database with separate schemas, in-process calls — simpler to build, test, deploy and debug.
- Move a module to a microservice when it needs independent scaling, release cadence, technology or team ownership.
- Microservices add network failures, distributed data and transactions (sagas/outbox), observability and DevOps cost — accept that only for a concrete benefit.
In short: The CORS policy must include the production SPA origin exactly (scheme + host + port, no trailing slash);
UseCorsmust run beforeUseAuthentication/UseAuthorizationand before the endpoints;- Azure App Service's own CORS setting can override the app — use one or the other;
- the "CORS error" can also hide a 500 or 404 (a failed preflight or an error page without CORS headers) — check the API logs and the Network tab for the actual status.
In short: That is an IDOR (Insecure Direct Object Reference) — broken object-level authorization, the #1 item in the OWASP API Security Top 10.
- Authentication alone is not enough: the API must check ownership on every request, e.g. filter by the current user's customer id from the token (
Where(o => o.CustomerId == currentUser.CustomerId)) or use resource-based authorization (IAuthorizationService.AuthorizeAsync(User, order, "OwnsOrder")). - Return 404 (not 403) to avoid revealing that the id exists, and add tests for it.
In short: Stabilise first: roll back or scale up if users are blocked.
- Then find the top queries by CPU (Query Store /
sys.dm_exec_query_stats), compare plans before and after the release — usually a new query without an index, a changed LINQ query producing a bad plan, an N+1 loop, or parameter sniffing. - Fix with an index, a rewritten query, forcing the last good plan in Query Store as a stop-gap, and add a performance test or alert so it is caught earlier next time.
In short: UI: upload control with file type/size checks, shows progress, then a result summary.
- API:
POSTmultipart, validate size, store the file (Blob Storage), return202with an import id. - Worker: read with a streaming library (e.g. ClosedXML/ExcelDataReader), validate every row and collect errors, insert in batches (
SqlBulkCopyor EF Core bulk/ExecuteUpdate) inside a transaction or into a staging table thenMERGE. - Status endpoint/SignalR for progress; downloadable error report; idempotent so a retry doesn't duplicate rows; audit who uploaded what.
A value type variable holds the data itself (int, bool, decimal, DateTime, any struct/enum). Assigning it copies the value. A reference type variable holds a reference to an object on the managed heap (class, string, arrays, delegates, record). Assigning it copies the reference, so two variables can point to the same object.
var a = new List<int>(); var b = a; b.Add(1); // a.Count == 1 — same object
int x = 1; int y = x; y++; // x is still 1Value types are stored inline (stack for locals, inside the parent object for fields); reference-type objects always live on the heap and are cleaned up by the GC.
string and StringBuilder?In short: string is immutable: every concatenation creates a new string object, so building a string in a loop causes many allocations (O(n²) copying).
StringBuilderkeeps a mutable buffer and appends in place; callToString()once at the end.- Use
StringBuilderfor loops or many appends, plain strings/interpolation for a handful of pieces.
const vs readonly vs static readonly.const: compile-time constant, implicitly static, only primitives/strings; the value is baked into callers at compile time.readonly: instance field assigned at declaration or in a constructor; can differ per object.static readonly: one value per type, computed at runtime (e.g.static readonly HttpClient Client = new()).
Gotcha: if library A has public const int Max = 10 and app B uses it, changing it to 20 in A does nothing in B until B is recompiled. Use static readonly for values that may change.
In short: Boxing converts a value type to object (or an interface) by allocating a heap object and copying the value; unboxing casts it back to the exact value type.
- It costs an allocation, a copy and GC pressure.
- Typical sources: non-generic collections (
ArrayList),string.Formatwith value-type arguments on old APIs, calling interface methods on structs via the interface. - Generics remove most boxing.
== and .Equals()?In short: For reference types, == compares references unless the operator is overloaded; Equals() is virtual and can be overridden to compare content.
stringoverloads both to compare text.- Records generate value-based
Equalsand==. - For value types,
Equalscompares content. - When you override
Equals, also overrideGetHashCode(objects that are equal must have the same hash code, or dictionaries break).
ref, out and in parameters?All three pass an argument by reference:
ref— must be initialised before the call; the method can read and change it.out— need not be initialised; the method must assign it (used byTryParse).in— read-only reference; avoids copying large structs.
decimal instead of double for money?In short: double is binary floating point and can't represent many base-10 fractions exactly (0.1 + 0.2 != 0.3), so totals drift by tiny amounts that show up in invoices and reports.
decimalstores base-10 digits exactly with 28–29 significant digits.- Trade-off:
decimalis slower and uses 16 bytes, so usedoublefor scientific/graphics maths anddecimalfor money and quantities that must add up exactly. - In SQL Server map it to
DECIMAL(18,2), notFLOAT.
Parse, TryParse and Convert?In short: int.Parse("x") throws FormatException for bad input (and ArgumentNullException for null).
int.TryParse(s, out var n)returnsfalseinstead of throwing — use it for user input.Convert.ToInt32(value)accepts many types (object, double, string), returns 0 fornull, and rounds doubles to the nearest even number (Convert.ToInt32(2.5)→ 2) whereas a cast truncates ((int)2.9→ 2).- Pass a
CultureInfowhen parsing numbers and dates from files or APIs.
Dictionary and Hashtable?In short: Dictionary<TKey,TValue> is generic: type-safe, no boxing, faster; a missing key with the indexer throws KeyNotFoundException (use TryGetValue).
Hashtableis the old non-generic version storingobjectkeys/values: needs casts, boxes value types, and returnsnullfor a missing key.- Both are hash tables with O(1) average lookup.
- For multiple threads use
ConcurrentDictionary.
In short: A multidimensional (rectangular) array int[,] is one object where every row has the same length, accessed as a[r, c].
- A jagged array
int[][]is an array of arrays — each row is a separate array and can have a different length, accessed asa[r][c]. - Jagged arrays are more flexible and often faster in .NET; rectangular arrays suit fixed grids such as matrices.
IEnumerable, ICollection, IList and IQueryable.IEnumerable<T>: forward-only iteration (foreach), deferred execution with LINQ to Objects, runs in memory.ICollection<T>: addsCount,Add,Remove,Contains.IList<T>: adds index access andInsert.IQueryable<T>: builds an expression tree that a provider (EF Core) translates to SQL and runs in the database.
db.Orders.Where(o => o.Total > 100).ToList(); // IQueryable: WHERE in SQL
db.Orders.AsEnumerable().Where(o => o.Total > 100).ToList(); // loads ALL rows then filters in memoryIn short: A delegate is a type-safe function pointer (Func, Action, custom delegate).
- It can be multicast.
- An event wraps a delegate so outside code can only
+=/-=handlers; only the declaring class can raise it. - This prevents subscribers from wiping out other handlers (
= null) or raising the event themselves. - Pattern: `public event EventHandler
? - Changed;
and raise withChanged?.Invoke(this, args)`.
In short: No new thread is created by await.
- The compiler turns the method into a state machine.
- When it hits
awaiton an incomplete task, the method returns to its caller and the thread goes back to the pool. - When the I/O completes, a continuation resumes the state machine — on the captured synchronisation context if there is one (UI thread), otherwise on a thread-pool thread.
- CPU-bound work still needs
Task.Runor parallelism to move off the current thread.
Task.WhenAll and awaiting tasks one by one?In short: Awaiting sequentially makes total time the sum of the calls; starting all tasks and then await Task.WhenAll(...) makes it roughly the longest single call.
WhenAllthrows the first exception when awaited, but the returned task'sExceptionholds all of them.- Use
WhenAnyfor "first to finish" or timeouts.
async void dangerous?In short: Callers can't await it, so they can't know when it finishes, and an exception thrown inside crashes the process (it is raised on the synchronisation context instead of being stored in a Task).
- Use
async Taskeverywhere except event handlers.
IDisposable and the using statement? Dispose vs Finalize?In short: IDisposable.Dispose() releases unmanaged resources (connections, file handles) deterministically.
usingguaranteesDisposeis called even if an exception is thrown (compiles totry/finally).- A finaliser is called by the GC at an unpredictable time and costs an extra GC cycle; it is only a safety net for raw unmanaged handles.
- In
DisposecallGC.SuppressFinalize(this)if a finaliser exists. - Most code just wraps
SafeHandleand never writes a finaliser.
In short: The heap is split into Gen 0 (new objects, collected very often and cheaply), Gen 1 (survivors of Gen 0, a buffer), and Gen 2 (long-lived objects, collected rarely and expensively).
- Most objects die young, so collecting Gen 0 frequently is efficient.
- Objects of 85,000 bytes or more go to the Large Object Heap, collected with Gen 2. .NET has workstation and server GC modes, plus background (concurrent) GC for Gen 2.
In short: Derive from Exception, end the name with Exception, provide the standard constructors (parameterless, message, message + inner exception) plus constructors/properties for useful data (e.g. Balance, Requested).
- Create one only when callers need to catch that specific situation (e.g.
InsufficientFundsException→ show a friendly message or return 409); - otherwise throw the built-in ones (
ArgumentException,InvalidOperationException). - Always pass the inner exception when wrapping.
lock(sugar forMonitor.Enter/Exit): mutual exclusion inside one process, synchronous only.SemaphoreSlim: limits concurrency to N and supportsWaitAsync, so use it when you mustawaitinside the critical section.Interlocked: atomic operations on single variables (increment, compare-exchange), lock-free.ConcurrentDictionary/ConcurrentQueue/Channel<T>: built-in thread-safe structures.- Immutability: data that never changes needs no locks.
- Across processes/servers: database locks, distributed locks (Redis, blob leases),
Mutexfor the same machine.
In short: Classic case: in a UI app or old ASP.NET (with a synchronisation context), code calls task.Result.
- The blocked thread holds the context; the awaited method's continuation needs the same context to resume, so both wait forever.
- Fixes: go async all the way (
awaitinstead of.Result), useConfigureAwait(false)in library code, and never mix blocking and async calls. - ASP.NET Core has no synchronisation context, but blocking still causes thread-pool starvation under load.
+= on a string and takes minutes. What do you do?In short: String concatenation in a loop copies the whole string every time, so the work is O(n²).
- Replace it with a
StringBuilder, or better, stream the rows straight to the output (StreamWriter, orPipeWriterin ASP.NET Core) so the full file is never held in memory. - Measure before and after with a stopwatch or BenchmarkDotNet.
- For huge exports, page the database query and write as you read.
- Confirm the trend with
dotnet-counters(GC heap size, Gen 2 count, LOH size). - Take memory dumps a few minutes apart with
dotnet-dump collect(ordotnet-gcdump) and compare which types grow, using Visual Studio, PerfView ordotnet-dump analyze(dumpheap -stat,gcroot). - Usual culprits: static collections or caches with no eviction, event handlers never unsubscribed,
HttpClientcreated per request, undisposed streams/DbContexts, large byte arrays on the LOH, unboundedMemoryCache. - Fix (size-limited cache,
IHttpClientFactory,using, weak events), then load-test to confirm.