.NET Full Stack Developer Interview — End to End
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: Optimistic concurrency: a ROWVERSION column mapped as a concurrency token ([Timestamp]).
- The UI sends back the version it loaded; EF Core adds
WHERE Id = @id AND RowVersion = @vto the UPDATE. - If no row is updated, EF throws
DbUpdateConcurrencyException; the API returns 409 Conflict and the UI shows "this record changed — reload". - Pessimistic locking (holding locks) is rarely right for web apps.
In short: appsettings.json + appsettings.{Environment}.json + environment variables, bound to strongly typed options (IOptions<T>, validated on start).
- Secrets: User Secrets locally, Azure Key Vault in the cloud, accessed with Managed Identity (no passwords in config).
- The SPA gets environment settings at build time (Angular
environment.ts/ Vite env) or from a/config.jsonloaded at startup so the same build can go to every environment.
In short: On PR: restore, build API and UI, run unit tests, lint, code analysis, maybe SonarQube and dependency scanning.
- On merge to main: build artefacts or Docker images with a version tag, deploy to dev, run DB migration scripts, smoke/integration tests, deploy to QA/UAT with approvals, then production via deployment slots or rolling update;
- rollback = redeploy the previous artefact.
- Pipelines are YAML in the repo.
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: Generate the contract from the API: ASP.NET Core produces an OpenAPI document (built-in AddOpenApi in .NET 9+, or Swashbuckle/NSwag);
- generate TypeScript clients/types with NSwag, openapi-typescript or Orval in the build.
- Version breaking changes (
/api/v2or header versioning with Asp.Versioning), keep changes additive, and run contract or end-to-end tests in CI.
In short: Don't do it inside the HTTP request.
- Return
202 Acceptedwith a job id, push a message to a queue (Azure Service Bus / RabbitMQ / Hangfire / aChannel<T>+BackgroundServicefor simple cases), process it in a worker, store status, and let the UI poll or receive a push via SignalR. - Make the job idempotent and retryable.
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.
In short: Sharing tables couples deployments and schema changes between teams.
- Prefer one owner of the data: other services call its API or keep a read-only copy updated through events (CustomerUpdated on a message bus).
- If they truly must share, use a view or a stable read schema as the contract.
- Inside a modular monolith, keep separate schemas and access other modules only through their public interfaces.
In short: Open DevTools console and Network: typical causes are a wrong base href / base path, 404s for JS bundles, the server not rewriting deep links to index.html (SPA fallback), missing environment config (API URL points to localhost), mixed content (HTTPS page calling HTTP API) or a runtime error in production mode only.
- Fix the hosting rewrite rule, base path and environment config, and add a smoke test to the pipeline.
In short: Order and stock in the same database transaction (SaveChangesAsync on one DbContext, or an explicit transaction).
- The email is a side effect outside the database, so use the outbox pattern: in the same transaction insert an "OrderPlaced" row into an Outbox table;
- a background worker reads it, publishes/sends the email and marks it done.
- If the app crashes after commit, the worker still sends it; consumers are idempotent so a retry doesn't send twice.