Dotnet Webapi Interview Questions
Dotnet Webapi Interview Questions
100
QUESTIONS
9
CATEGORIES
3
LEVELS
By Mukesh Murugan
[Link]
Testing
10 questions
2 Junior 5 Mid 3 Senior
Production Readiness
10 questions
1 Junior 5 Mid 4 Senior
I'd default to offset-based pagination for simple use cases — ?page=1&pageSize=20 . The
response includes metadata so the client isn't guessing:
{ JSON
"data": [...],
"pagination": {
"currentPage": 1,
"pageSize": 20,
"totalCount": 487,
"totalPages": 25,
"hasNextPage": true,
"hasPreviousPage": false
}
}
But here's the thing — totalCount requires a COUNT(*) query which gets expensive on large
tables. I've seen APIs slow down just because of that count. For high-volume endpoints, I'd
switch to cursor-based pagination (keyset pagination) using the last item's ID: ?
cursor=abc123&pageSize=20 . It's faster because it uses an index seek instead of offset skip.
I'd also set a max page size (say 50) server-side so a client can't request pageSize=10000 and
kill the database.
Red Flag: I'd just use .Skip() and .Take() in EF Core." — Shows no awareness of
performance implications or response design.
Follow-up: What happens to offset pagination when a record is deleted between page
requests? How does cursor-based pagination handle this?
GREAT ANSWER
It depends on what the client cares about. If the resource itself is created synchronously and the
email is just a side effect, I'd return 201 Created with a Location header pointing to the new
resource. The email is fire-and-forget — the client doesn't need to know about it.
But if the entire operation is async — say the resource creation itself is queued — then 202
Accepted is correct. It means "I got your request, I'll process it later." I'd include a status
endpoint URL so the client can poll: Location: /api/orders/123/status .
The worst thing you can do is return 200 OK for a creation endpoint. It tells the client nothing.
Red Flag: 200 OK, because it worked." — Doesn't understand REST semantics.
Follow-up: When would you return 204 No Content vs 200 OK on a PUT endpoint?
There are a few approaches, and I've used different ones depending on the situation:
Option 1 — JsonPatchDocument ([Link]):
The client sends JSON Patch operations ( [{"op": "replace", "path": "/email", "value":
"new@[Link]"}] ). It works but the client-side experience is awkward.
Option 2 — Nullable DTOs with a "fields sent" tracker: I prefer a simpler approach where the
DTO uses Optional<T> wrapper or I check [Link] to see which fields were
actually in the JSON body. This way null means "clear this field" and "missing" means "don't
touch it."
Option 3 — Just use PUT with full replacement. Honestly, for most internal APIs, this is fine.
PATCH adds complexity, and if your entities are small, just send the whole thing.
My take: unless you have a strong reason for PATCH (large entities, bandwidth constraints,
mobile clients), a well-designed PUT is simpler and less error-prone.
Red Flag: I'd just use a regular DTO and check for nulls." — Doesn't understand the null vs
missing distinction.
Follow-up: How would you validate a PATCH request? Can you apply FluentValidation to a
partial update?
Red Flag: I'd return everything and let the client ignore what it doesn't need." — Wastes
bandwidth and exposes unnecessary data.
Follow-up: How do you version your API when the mobile app and web app evolve at different
speeds?
Follow-up: How does [Link] Core bind parameters from different sources — route, query,
body, header? What's the binding order?
I'd create a dedicated bulk endpoint: POST /api/products/bulk . The request body is an array of
product DTOs. But the real question is: what happens when item 347 out of 500 fails validation?
Option A — All-or-nothing: Wrap everything in a transaction. If one fails, all fail. Return 400 with
the specific validation errors. Simple but frustrating for the client — they have to fix one item and
resubmit all 500.
Option B — Partial success: Process each item independently. Return 207 Multi-Status with
per-item results:
{ JSON
"succeeded": 498,
"failed": 2,
"errors": [
{ "index": 347, "error": "Name is required" },
{ "index": 412, "error": "Duplicate SKU" }
]
}
I prefer Option B for most cases. I'd also set a max batch size (500-1000), use
ExecuteUpdateAsync or BulkExtensions instead of calling SaveChangesAsync 500 times, and
consider making it async with 202 Accepted if processing takes more than a few seconds.
Red Flag: I'd just loop through and call the single-create endpoint 500 times." — Shows no
understanding of performance or transaction boundaries.
Follow-up: How would you handle idempotency for bulk operations? What if the client retries
after a timeout?
Red Flag: I'd just change the existing endpoints and tell clients to update." — Breaks existing
integrations.
Follow-up: How do you handle versioning in your OpenAPI documentation? Can Scalar show
multiple versions?
In [Link] Core, Kestrel has a default max request body size of ~28.6 MB. That's way too high
for most APIs.
I'd configure it at multiple levels:
Global ([Link]):
When exceeded, Kestrel returns 413 Payload Too Large automatically. But I'd also add request
size validation in middleware to return a proper ProblemDetails response instead of the raw
413.
Don't forget: if you're behind a reverse proxy (Nginx, Azure App Gateway), you need to configure
the limit there too. I've seen cases where Kestrel's limit was correct but Nginx had a 100MB
default and happily forwarded massive payloads.
Follow-up: How would you rate-limit file uploads specifically, separate from regular API rate
limiting?
The correct HTTP response is 406 Not Acceptable . The client asked for a format the server
can't provide.
In [Link] Core, the default behavior is... not great. By default, it'll just return JSON regardless
of the Accept header. To enforce content negotiation properly:
For minimal APIs, content negotiation is simpler — [Link](data) returns JSON. If you
genuinely need XML support, add AddXmlSerializerFormatters() .
My take: unless you have a specific client that needs XML (enterprise/SOAP integrations), don't
add XML support. It increases your attack surface and doubles your serialization testing.
Red Flag: Just return JSON anyway, nobody uses XML." — Correct instinct, but shows no
awareness of HTTP standards or how to configure it properly.
Follow-up: How does content negotiation work with custom media types like
application/[Link]+json?
These attributes tell [Link] Core where to look for a parameter value:
- [FromRoute] — URL path segments: /api/products/{id}
- [FromQuery] — Query string: ?search=phone
- [FromBody] — Request body (JSON usually)
- [FromHeader] — HTTP headers: X-Correlation-Id
For controllers, [Link] Core infers the source: simple types come from route/query, complex
types from body. But this inference can go wrong — if you have a complex type as a query
parameter (like a filter DTO), you need [FromQuery] explicitly.
For minimal APIs, the rules are slightly different. Complex types default to [FromBody] , and you
must be explicit about [FromQuery] for complex types.
The real gotcha: you can only have one [FromBody] parameter per endpoint. If you need
multiple body values, wrap them in a single DTO.
Red Flag: I never use those attributes, it just works automatically." — Works until it doesn't.
Follow-up: How does [AsParameters] work in minimal APIs, and how does it differ from
[FromBody]?
Never make the client wait 5 minutes on an HTTP request. I'd use the async request-reply
pattern:
Step 1 — Accept the request:
POST /api/reports
→ 202 Accepted
→ Location: /api/reports/abc123/status
GET /api/reports/abc123/status
→ 200 OK
→ { "status": "processing", "percentComplete": 45 }
GET /api/reports/abc123/status
→ 200 OK
→ { "status": "completed", "downloadUrl": "/api/reports/abc123/download" }
Red Flag: I'd increase the request timeout to 10 minutes." — Shows no understanding of async
patterns.
Follow-up: How do you handle the case where the background job fails? How does the client
find out?
Why interviewers ask this: Critical for payment APIs, order processing — anywhere duplicate requests
cause real damage.
GREAT ANSWER
Key decisions: the idempotency key should expire (24h is typical), and you need to handle the
race condition where two identical requests arrive simultaneously (use a distributed lock or
database unique constraint on the key).
Red Flag: GET and PUT are already idempotent, so it's not an issue." — Misses POST entirely.
Follow-up: How do you handle idempotency when the request succeeds but the response is
lost due to a network error?
Order matters because middleware runs top-to-bottom on the request, bottom-to-top on the
response. Here's the correct order:
What breaks with wrong order: - CORS after auth → preflight OPTIONS requests get 401
Unauthorized - Exception handler not first → unhandled exceptions in early middleware crash
with no ProblemDetails response - Rate limiter after auth → brute force attackers still hit your auth
middleware - Auth before CORS → browser CORS errors that look like auth errors
I've seen a production bug where CORS was registered after authentication. The API worked fine
in Postman but failed in the browser. Took hours to debug because the error message was
misleading.
Red Flag: I just put them in whatever order and it works." — Works in dev, fails in production.
Follow-up: Where would you put response compression middleware? Before or after static
files?
This is the captive dependency problem. The scoped service gets captured by the singleton and
lives forever — it never gets disposed when the scope ends. If that scoped service holds a
database connection (like DbContext ), you now have a connection that's shared across all
requests. You'll get threading issues, stale data, and eventually connection pool exhaustion.
Detection: In .NET, add ValidateScopes and ValidateOnBuild in development:
Red Flag: I'd just register everything as singleton to avoid the issue." — Makes it worse.
Follow-up: What's the difference between Transient and Scoped in the context of a Web API
request?
Middleware runs on every request and has access to the raw HTTP pipeline. It sees requests
before routing happens. Use middleware for cross-cutting concerns like logging, CORS,
exception handling, and authentication.
Endpoint filters run only on matched endpoints and have access to the endpoint's parameters
and return type. They're like mini-middleware scoped to specific endpoints.
My rule: if it needs to run on every request or before routing, use middleware. If it's specific to
certain endpoints and needs parameter access, use filters.
Filters are perfect for validation, logging specific endpoints, caching headers, or transforming
responses.
Red Flag: They're the same thing." — They operate at different levels of the pipeline.
Follow-up: How do endpoint filters compose? What's the execution order when you have
multiple filters?
Instead of a class creating its own dependencies (using new ), the dependencies are provided
from outside — "injected" into the constructor.
[Link] Core has a built-in DI container because: 1. Testability — swap real implementations
with mocks 2. Lifetime management — the container handles when objects are created and
disposed (singleton, scoped, transient) 3. Loose coupling — classes depend on interfaces, not
concrete types 4. Configuration — swap implementations without changing consuming code
(different email provider? Just change the registration)
The three lifetimes matter: Singleton (one instance forever), Scoped (one per HTTP request),
Transient (new instance every time). DbContext is scoped because each request needs its
own database connection.
Red Flag: It's a design pattern where you inject things into classes." — Too vague, no mention
of why.
I'd use a layered approach with ProblemDetails (RFC 9457) as the standard format:
[Link](); CSHARP
[Link]();
[Link]();
[Link] = [Link]!.Value;
await [Link](response);
});
});
Red Flag: I catch all exceptions in a try-catch in every controller action." — Massive code
duplication.
Follow-up: How does the Result pattern work? Show me how you'd use it in a service method
and map it to an HTTP response.
Why interviewers ask this: Tests DI container knowledge beyond the basics.
GREAT ANSWER
The second registration wins — when you inject IProductService , you get the last registered
implementation. But here's the catch: the first registration isn't removed. Both are in the
container.
If you inject IEnumerable<IProductService> , you get both implementations. This is actually
useful for the decorator pattern or chain of responsibility.
If you want to guarantee only one registration, use TryAddScoped :
This is how library authors prevent overriding user registrations — they use TryAdd* so the
consumer's registration takes priority.
Red Flag: It throws an error." — It doesn't, which is why it's a subtle source of bugs.
Follow-up: How would you implement the decorator pattern using DI — wrapping a service
with logging or caching without the consuming code knowing?
Behind a reverse proxy, the client's real IP, scheme (HTTP/HTTPS), and host are lost. The proxy
forwards them in headers like X-Forwarded-For , X-Forwarded-Proto , and X-Forwarded-Host .
Without configuring this, your API thinks every request comes from the proxy's IP (often
[Link] ), and [Link] is always http even if the client used HTTPS.
This breaks: - IP-based rate limiting (everyone looks like the same IP) - HTTPS redirect loops
(API sees HTTP, redirects to HTTPS, proxy forwards as HTTP again) - Logging (you log the proxy
IP, not the client IP)
The fix:
In Azure App Service or container environments, you might also need to set
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true .
Red Flag: I just deploy to Kestrel directly in production." — Kestrel shouldn't be directly
exposed to the internet.
Follow-up: Why shouldn't Kestrel be exposed directly to the internet? What does a reverse
proxy provide?
Why interviewers ask this: Simple task, but reveals whether someone understands middleware lifecycle.
GREAT ANSWER
The key detail: I use OnStarting instead of setting the header before await next() . Why?
Because if downstream middleware changes the response (like a redirect), headers set before
next() might get cleared. OnStarting runs right before headers are sent to the client —
guaranteed to work.
I also add the same requestId to the logging scope so I can correlate logs with client-reported
request IDs:
Red Flag: I'd set [Link] before calling next()." — Works most of the
time but fragile.
Red Flag: I'd just restart the service periodically." — That's a workaround, not a fix.
Follow-up: What's the difference between a managed memory leak and a native memory leak
in .NET?
The practical rule: if you're in a scoped service (controller, endpoint handler), use
IOptionsSnapshot<T> . If you're in a singleton (background service, hosted service), use
IOptionsMonitor<T> with CurrentValue .
Common mistake: using IOptions<T> everywhere and then wondering why config changes
from [Link] don't take effect without restarting the app.
Red Flag: I always use IOptions<T>, I didn't know there were other versions.
.NET has built-in rate limiting since .NET 7. I'd use [Link] :
[Link] = StatusCodes.Status429TooManyRequests;
});
[Link]();
[Link]("/api/products", GetProducts).RequireRateLimiting("api");
Algorithm choice depends on the use case: - Fixed window — simple, good for most APIs.
Weakness: burst at window boundaries (200 requests if 100 at end of window 1 + 100 at start of
window 2) - Sliding window — smooths out the boundary burst problem. Better for APIs with
consistent traffic - Token bucket — allows controlled bursts. Good for APIs where occasional
spikes are OK (like search) - Concurrency limiter — limits simultaneous requests, not requests
per time. Good for expensive endpoints (report generation)
For production, I'd use sliding window by default and partition by client IP or API key:
I'd also return Retry-After header in the 429 response so clients know when to retry.
Red Flag: I'd build my own rate limiter with a dictionary." — Don't reinvent thread-safe
infrastructure.
Follow-up: How would you implement different rate limits for authenticated vs anonymous
users?
Why interviewers ask this: Tests understanding of minimal APIs vs controller-based APIs.
GREAT ANSWER
MapGet , MapPost , MapPut , MapDelete , MapPatch — these are minimal API endpoints. You
define routes and handlers inline:
Map is the generic version — it matches all HTTP methods for a path. Rarely used for APIs.
MapGroup is for grouping endpoints with shared prefixes, filters, or metadata:
My preference: minimal APIs for new projects. They're simpler, faster (no reflection), and more
explicit. Controllers still make sense for large APIs where you want the organizational structure.
Red Flag: I only use controllers, I haven't used minimal APIs." — Minimal APIs are the default in
.NET templates since .NET 6.
Follow-up: Can you mix minimal APIs and controllers in the same project? When would you?
That's the N+1 query problem. One query fetches 200 products, then EF Core lazily loads a
navigation property (like Category ) for each product individually — 200 extra queries.
How it happens:
Red Flag: I'd disable lazy loading." — That masks the problem, doesn't fix it.
Never apply migrations at app startup in production. Here's why: if you have multiple instances
(pods, app services), they'll all try to run the migration simultaneously. You'll get lock contention,
timeouts, or duplicate migration attempts.
My approach:
1. 1. Generate a SQL script from the migration:
1. 2. Review the SQL — never blindly apply generated migrations. Check for table locks, data
loss, index creation on large tables.
1. 3. Apply via CI/CD pipeline — a dedicated migration step that runs before the application
deployment:
1. 4. Use idempotent scripts ( --idempotent flag) so re-running the same migration is safe.
For teams, I also recommend: never modify a migration after it's been applied to any
environment. Create a new migration instead.
Follow-up: How do you handle a migration that needs to be rolled back in production?
This is a concurrency conflict. I'd use optimistic concurrency control with a concurrency token:
[Timestamp]
public byte[] RowVersion { get; set; } = null!;
}
EF Core automatically includes the RowVersion in the WHERE clause of UPDATE statements:
If another user changed the row since it was read, the WHERE clause matches zero rows, and EF
Core throws DbUpdateConcurrencyException .
I handle it like this:
try CSHARP
{
[Link] = [Link];
await [Link]();
}
catch (DbUpdateConcurrencyException)
{
return [Link](new ProblemDetails
{
Title = "Concurrency Conflict",
Detail = "This product was modified by another user. Please refresh and try agai
n."
});
}
The client receives a 409 Conflict and can reload the latest data.
Pessimistic locking (database-level row locks) is an alternative but doesn't scale well — it holds
locks and blocks other readers/writers.
Red Flag: Last write wins — whoever saves last gets their change." — Data loss.
By default, EF Core tracks every entity it queries. It keeps a snapshot of the original values so it
can detect changes when you call SaveChangesAsync() . This tracking has a memory and CPU
cost.
Use AsNoTracking() when: - Read-only queries (GET endpoints that just return data) -
Reporting or dashboard queries - Any query where you won't modify the returned entities
For APIs where most endpoints are read-heavy, I configure NoTracking as the default:
Red Flag: I always use AsNoTracking() because it's faster." — Then how do you update
entities?
I always prefer Fluent API over data annotations because it keeps the domain model clean and all
configuration is in one place. I also always specify OnDelete behavior explicitly — the default
cascade delete can be dangerous.
Red Flag: I use [ForeignKey] attribute on the property." — Works but doesn't scale, and you
lose explicit delete behavior configuration.
I'd use a global query filter so soft-deleted records are automatically excluded from all queries:
Step 1 — Base entity with IsDeleted flag:
When you need to include deleted records (admin panel, audit trail):
Follow-up: How do global query filters interact with Include? Are related entities also
filtered?
Raw SQL is acceptable when: - EF Core can't generate an efficient query (complex reporting,
window functions) - You need database-specific features (full-text search, JSON queries) -
Performance-critical queries where you need exact control
Safe approach — parameterized queries:
// SAFE — parameterized via string interpolation (EF Core converts to parameters) CSHARP
var products = await [Link]
.FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice}")
.ToListAsync();
Red Flag: I concatenate user input into the SQL string." — SQL injection waiting to happen.
Follow-up: Can you mix EF Core LINQ and raw SQL? For example, apply .Where() on top of a
FromSql() call?
var query = [Link](p => [Link] > 100).Include(p => [Link]); CSHARP
var sql = [Link](); // Log this
Step 2 — Run the SQL in SSMS/pgAdmin with execution plan: Look for table scans (missing
indexes), key lookups, and sorts.
Common culprits and fixes:
1. 1. Missing index — add an index on filtered/sorted columns:
1. 2. Loading too much data — use projection instead of loading full entities:
Follow-up: When would you consider using Dapper instead of EF Core for a specific query?
Why interviewers ask this: Shows deeper EF Core knowledge beyond basic CRUD.
GREAT ANSWER
Value converters transform property values when reading from and writing to the database. The
property type in your C# model can differ from the column type in the database.
Common use cases:
1. 1. Enums as strings (instead of integers):
1. 2. Strongly-typed IDs:
Gotcha: Value converters prevent EF Core from translating certain LINQ queries to SQL. If you
filter by an enum stored as string, EF Core handles it. But complex converter logic might force
client-side evaluation.
Follow-up: How do value converters interact with migrations? What happens to existing data
when you add a converter to an existing property?
Find() Checks change tracker first, then database Maybe no SQL at all
Red Flag: They're all the same, I just use FirstOrDefault everywhere.
[Link]( CSHARP
new Category { Id = 1, Name = "Electronics" },
new Category { Id = 2, Name = "Clothing" }
);
Pros: tracked in migrations, reproducible. Cons: requires hardcoded PKs, can't use navigation
properties, becomes unwieldy for large datasets.
2. Custom initialization logic in [Link] or a hosted service:
Pros: flexible, can use computed values and navigation properties. Cons: runs at startup, not
tracked in migrations.
3. SQL scripts in the migration:
[Link](...); CSHARP
// or
[Link]("INSERT INTO Categories ...");
Pros: full control, tracked in migrations. Cons: SQL-specific, can break on database provider
changes.
My preference: HasData() for small reference data (countries, statuses, roles). Custom
initialization for development seed data. SQL scripts for complex production data migrations.
Red Flag: I manually insert data in the database." — Not reproducible or testable.
Follow-up: How do you handle seed data that needs to be different per environment (dev vs
production)?
Before EF Core 7, you had to load all entities, modify them, and save — terrible for large datasets:
This generates a single UPDATE Products SET IsActive = 0 WHERE CategoryId = 5 . No entities
loaded, no change tracking overhead.
Similarly, for bulk deletes:
Important caveats: - These bypass the change tracker — SaveChanges interceptors and events
won't fire - Global query filters still apply (so soft-deleted records are excluded) - No cascade
deletes through EF Core — the database must handle cascades
Red Flag: I'd load all the entities and update them in a loop." — Shows no awareness of bulk
operations.
Follow-up: If you need audit logging on these bulk updates, how do you handle it since the
change tracker is bypassed?
This is the fundamental JWT trade-off — JWTs are self-contained and can't be revoked once
issued. Options:
1. Short-lived access tokens (my preferred approach): Keep access tokens short (5-15 minutes)
and use refresh tokens. When the access token expires, the refresh token exchange checks the
latest role from the database. The window of exposure is the access token lifetime.
2. Claims refresh on sensitive operations: For critical endpoints (admin actions, payment), re-
validate the user's current roles from the database:
CSHARP
[Link]("/api/admin/users/{id}", async (int id, ClaimsPrincipal user, UserService
users) =>
{
// Don't trust the JWT claim for destructive operations
var currentRoles = await [Link]([Link]());
if ()
return [Link]();
// proceed
});
3. Token blocklist (last resort): Store revoked token IDs in Redis. Check on each request. This
defeats the purpose of JWTs (stateless) but sometimes you need it for compliance.
My take: Short access tokens (5 min) + refresh tokens handles 95% of cases. Add real-time
validation only for destructive operations. A full blocklist is overkill unless you have regulatory
requirements.
Red Flag: JWT tokens can't be revoked, so there's nothing you can do." — True in theory,
dangerous in practice.
Follow-up: How do refresh tokens work? Where do you store them, and how do you handle
refresh token rotation?
Roles are coarse-grained ("Admin", "User"). Policies let you combine multiple requirements:
[Link]() CSHARP
.AddPolicy("CanEditProduct", policy =>
[Link]("Admin", "ProductManager")
.RequireClaim("department", "inventory")
.AddRequirements(new MinimumAgeRequirement(18)));
return [Link];
}
}
CSHARP
[Link]("/api/products/{id}", async (int id, IAuthorizationService auth, ClaimsPrinci
pal user) =>
{
var product = await [Link](id);
var result = await [Link](user, product, new ResourceOwnerRequirement
());
if (![Link]) return [Link]();
// update
});
This lets you answer questions like "can THIS user edit THIS product?" — not just "is this user an
admin?"
Red Flag: I check [Link]() in every endpoint." — Doesn't scale, duplicates logic.
[Link]([Link](1)); CSHARP
Common mistake I've seen: CORS configured correctly in [Link] but a reverse proxy
strips the CORS headers. Always test CORS from the actual browser, not Postman (Postman
doesn't enforce CORS).
Red Flag: I just use AllowAnyOrigin and AllowAnyMethod." — Open security hole.
Follow-up: How does CORS work with cookie-based authentication vs JWT bearer tokens?
[Link]( CSHARP
new Uri("[Link]
new DefaultAzureCredential());
My production setup: Azure Key Vault for secrets, environment variables for non-sensitive
config, appsettings.{Environment}.json for environment-specific settings that aren't secrets.
Red Flag: I put them in [Link] and add it to .gitignore." — Someone will
commit it eventually.
Follow-up: How does the .NET configuration system's precedence work? What overrides
what?
[Link]() CSHARP
.AddJwtBearer("Bearer", options => { /* JWT config */ })
.AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>("ApiKey", options => { });
[Link]()
.SetDefaultPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Bearer", "ApiKey")
.Build());
Key detail: returning [Link]() (not Fail ) when the header is missing
lets the next scheme (JWT) try. Fail would short-circuit.
Red Flag: I'd check for the API key manually in middleware before the auth middleware runs."
— Bypasses the authentication pipeline.
Follow-up: How do you rate-limit API key clients differently than JWT-authenticated users?
Mass assignment happens when you bind request data directly to your entity model. A client
could send {"name": "test", "isAdmin": true} and promote themselves.
Prevention — always use DTOs:
Rules: 1. Never bind directly to entity classes 2. Create separate DTOs for create, update, and
response 3. Map explicitly — don't use auto-mapping from request to entity without controlling
which fields are mapped 4. Validate that readonly fields (CreatedAt, Id, IsAdmin) can't be set
from the client
Red Flag: I use [Bind] attribute to exclude fields." — Fragile, easy to forget a field.
Follow-up: How does this interact with PATCH endpoints? How do you prevent over-posting
on partial updates?
Authentication = "Who are you?" — proving your identity. Authorization = "What are you
allowed to do?" — checking your permissions.
Real API example:
Status codes tell you which failed: - 401 Unauthorized — actually means "unauthenticated"
(bad or missing token) - 403 Forbidden — authenticated but not authorized (valid token,
insufficient permissions)
Yes, the HTTP status code names are misleading. 401 should really be called
"Unauthenticated."
Red Flag: They're the same thing — checking if the user can access the endpoint.
A naive implementation stores API keys in [Link] and compares strings. This has
problems: no auditing, no per-key permissions, timing attacks on string comparison, and you
can't rotate keys without redeploying.
Production-grade approach:
1. 1. Store hashed keys in the database:
[Link]( CSHARP
[Link]([Link](providedKey)),
Convert.FromBase64String(storedHash));
Red Flag: I compare the key with == against a string in my config." — Timing attacks, no
rotation, no auditing.
Follow-up: How would you implement API key scopes — like a key that can only read products
but not delete them?
// Endpoint
[Link]("/api/users/{id}", async (int id, AppDbContext db) =>
{
var user = await [Link]
.Where(u => [Link] == id)
.Select(u => new UserResponse([Link], [Link], [Link])) // Projection
.FirstOrDefaultAsync();
This works but is fragile — one developer forgets the attribute, or serialization settings change,
and you leak data. DTOs make it impossible to accidentally expose fields.
Additional safeguards: - Use projection ( .Select() ) to avoid loading sensitive data from the
database at all - Add response serialization tests that verify sensitive fields aren't present -
Never return entity objects directly from endpoints
Red Flag: I use [JsonIgnore] on the entity." — Works until someone bypasses it.
Follow-up: How would you handle a scenario where admin users should see more fields than
regular users?
I'd also configure the Content-Security-Policy if the API serves any HTML (error pages,
Scalar UI).
Red Flag: APIs don't need security headers, that's a frontend thing." — APIs are still HTTP,
headers still matter.
Follow-up: How do you verify security headers are correctly configured? What tools do you
use?
SQL Injection: - Use EF Core or parameterized queries — never concatenate user input into SQL
- If using raw SQL, use FromSqlInterpolated (parameterized) not FromSqlRaw with string
concatenation - EF Core's LINQ queries are safe by default
XSS (Cross-Site Scripting): - For APIs returning JSON, the risk is lower than HTML — JSON
serialization escapes by default - But if your API returns HTML (error messages, emails), sanitize
output - Never return raw user input in error messages: $"User {username} not found" — if
username contains script tags, you have XSS - Use [Link]() for any
user input rendered as HTML
CSRF (Cross-Site Request Forgery): - For APIs using JWT bearer tokens: CSRF is not a concern
because the browser doesn't automatically send the token (unlike cookies) - For APIs using
cookie authentication: add anti-forgery tokens or use SameSite=Strict cookies - For SPAs:
SameSite=Lax on auth cookies + verify Origin header
Additional protections: - Rate limiting on auth endpoints - Input validation with FluentValidation
on all endpoints - Request size limits - Disable detailed error messages in production
Red Flag: EF Core handles all of that automatically." — EF Core helps with SQL injection, not
XSS or CSRF.
Database queries EF Core logging, SQL Profiler Add indexes, use projection, fix N+1
External API calls HttpClient logging Add caching, make parallel with [Link]
Memory dotnet-counters GC
Reduce allocations, use Span<T>
allocation metrics
Red Flag: I'd add caching." — Caching what? You haven't found the bottleneck yet.
Follow-up: How would you set up continuous performance monitoring so you catch
regressions before users notice?
Why interviewers ask this: Caching is multi-layered. Tests whether someone picks the right tool.
GREAT ANSWER
Response Caching — sets HTTP headers ( Cache-Control , Expires ). The browser or CDN
caches the response. You have no server-side control over invalidation.
CSHARP
[Link]("/api/products", () => ...).CacheOutput(p => [Link]([Link]
(5)));
Output Caching — caches the full HTTP response on the server. Supports tag-based
invalidation:
My rule: Use output caching for read-heavy API endpoints. Use HybridCache for expensive
business logic or database queries that multiple endpoints share. Use response caching headers
for content that CDNs should cache.
Follow-up: What is cache stampede and how does HybridCache prevent it?
1. 2. Don't overdo it — parallel database queries on the same DbContext don't work. DbContext
is not thread-safe. You'd need separate scopes or separate DbContext instances.
1. 3. [Link] vs [Link] — WhenAll is for a known set of async tasks.
[Link] is for processing a collection with controlled concurrency.
Red Flag: I'd just await them one by one." — 3x slower for no reason.
Follow-up: What happens if you try to use the same DbContext in parallel tasks? How do you
solve it?
Response compression (gzip, brotli) reduces payload size, typically 60-80% for JSON/text
responses.
Red Flag: Always enable it, it makes everything faster." — Not for small responses, binary
content, or streaming.
Follow-up: How do you configure different compression levels for different endpoints?
// Correct
var product = await GetProductAsync(id);
3. Forgetting ConfigureAwait(false) in library code: Not relevant in [Link] Core (no sync
context), but critical in libraries that might be used in UI apps.
4. Fire-and-forget without error handling:
Red Flag: I don't use async, it's complicated." — Every I/O operation in an API should be
async.
Key decisions: 1. AsAsyncEnumerable() — EF Core streams rows from the database instead of
loading all 100K into memory 2. Write directly to [Link] — no intermediate buffering 3.
Disable response buffering — [Link]["Transfer-Encoding"] = "chunked" 4.
Periodic flushing — don't wait until all rows are written
Memory usage: Instead of loading 100K objects (potentially hundreds of MB), this uses constant
memory regardless of dataset size.
Red Flag: I'd load all products into a list, build a CSV string, and return it." — Out of memory at
scale.
Follow-up: How would you add progress reporting or cancellation support to this streaming
export?
Default in [Link]
Yes (since .NET Core 3.0) Needs AddNewtonsoftJson()
Core
Polymorphic
Supported (with [JsonDerivedType] ) Built-in and easier
serialization
Simpler JsonConverter<T>
Custom converters More verbose API
API
[JsonSerializable(typeof(ProductResponse))] CSHARP
internal partial class AppJsonContext : JsonSerializerContext;
Red Flag: Newtonsoft is better because I'm more familiar with it." — That's a personal
preference, not a technical reason.
Follow-up: How do [Link] source generators work, and what's the performance
improvement?
CSHARP
[Link] = "public, max-age=1800, stale-while-revalidate=6
0";
Stock prices (changes every second): - No HTTP caching — data is always stale - Use
SignalR/WebSockets for real-time push instead of polling - If polling is required: short-lived
HybridCache (5-10 seconds) to prevent database hammering when 1000 clients poll
simultaneously
Key insight: The caching layer depends on data volatility AND access patterns. High-read, low-
write data (catalog) = aggressive caching. High-volatility data (prices) = push-based or micro-
cache.
Red Flag: Cache everything for 5 minutes." — One size doesn't fit all.
Follow-up: How would you implement cache warming for the product catalog after a
deployment?
The CancellationToken signals when a client disconnects or the request is aborted. Without it,
your server keeps working on a request nobody is waiting for.
return [Link](report);
});
Why it matters: 1. Client navigates away → request is cancelled → server stops the DB query
immediately 2. Load balancer timeout → request cancelled → server frees resources 3.
Deployment/shutdown → ApplicationStopping token fires → graceful termination
Common mistakes: - Not passing it to EF Core queries, HTTP calls, or I/O operations - Catching
OperationCanceledException and treating it as a real error (it's not — the client left)
Propagate it everywhere:
Red Flag: I've never used CancellationToken in my endpoints." — Wasting server resources.
async/await frees up the thread while waiting for I/O operations (database queries, HTTP calls,
file reads). The thread goes back to the thread pool and can handle other requests.
Without async:
With async:
// Thread is RELEASED during the 200ms wait — handles other requests CSHARP
var data = await [Link]("[Link]
Why this matters for APIs: A web server has a limited thread pool (default ~100 threads). If every
request blocks a thread for 200ms, you can only handle ~500 requests/second. With async,
those threads handle other requests during the wait — you might handle 10,000+
requests/second with the same thread pool.
Key distinction: async doesn't make code faster. A single request still takes 200ms. But it
makes the server handle MORE concurrent requests because threads aren't wasted waiting.
When NOT to use async: CPU-bound work (calculations, JSON parsing). Async only helps with
I/O-bound work. For CPU work, use [Link]() to move it off the request thread.
Red Flag: Async makes code run faster." — It doesn't. It improves throughput, not latency.
Follow-up: What's the thread pool in .NET and how does it manage threads? What happens
when all threads are exhausted?
[MemoryDiagnoser] CSHARP
public class SerializationBenchmarks
{
private readonly Product _product = new() { Name = "Test", Price = 99.99m };
[Benchmark(Baseline = true)]
public string SystemTextJson() => [Link](_product);
[Benchmark]
public string Newtonsoft() => [Link](_product);
}
Why not Stopwatch ? 1. JIT compilation — first run is always slower (JIT warmup).
BenchmarkDotNet handles warmup automatically. 2. GC interference — garbage collection can
pause execution randomly. BenchmarkDotNet isolates GC effects. 3. Statistical significance —
one measurement means nothing. BenchmarkDotNet runs hundreds of iterations and reports
mean, median, and standard deviation. 4. Memory allocation tracking — [MemoryDiagnoser]
shows Gen0/Gen1/Gen2 collections and bytes allocated.
Common mistakes: - Benchmarking in Debug mode (no optimizations) - Not including memory
allocation metrics (faster code that allocates more can be worse) - Benchmarking too little or too
much code (isolate the thing you're comparing) - Dead code elimination — the JIT might optimize
away code that doesn't produce a used result
Follow-up: Can you benchmark async code with BenchmarkDotNet? What's different?
My take: you probably don't need a generic repository over EF Core. Here's why:
DbContext already IS a repository + unit of work. Adding IRepository<T> on top is often a
leaky abstraction:
When I WOULD use a repository: 1. Complex query logic that I want to unit test without a
database — wrap specific queries in a service/repository 2. When EF Core might be swapped
out — unlikely but happens in some architectures 3. Domain-Driven Design — aggregate root
repositories that enforce invariants
What I use instead: - Inject DbContext directly into handlers/services for simple CRUD - Create
focused query services for complex queries:
This gives me testability (mock ProductQueries ) without the ceremony of generic repositories.
Red Flag: Either extreme — "always use repositories" or "never use repositories" without
nuance.
Follow-up: In what architecture would you definitely use repositories? How does DDD change
your answer?
Why interviewers ask this: Architecture pattern that's widely discussed but often over-applied.
GREAT ANSWER
CQRS (Command Query Responsibility Segregation) means using different models for reading
and writing data. At its simplest:
When it's essential: - Read and write workloads have very different performance characteristics
(100:1 read/write ratio) - The domain model is complex but the read model is simple (e-
commerce, finance) - You need different data stores for reads vs writes (SQL for writes,
Elasticsearch for reads) - Event sourcing — CQRS is almost required with event sourcing
When it's overkill: - Simple CRUD APIs with no complex business logic - Small teams where the
overhead of separate models isn't worth it - When read and write models are nearly identical
(you're just duplicating code)
My approach: Start without CQRS. When I notice that my read queries are fighting with my
domain model (adding properties just for display, complex projections), that's the signal to split.
You don't need MediatR or a full CQRS framework. Separate handler classes are enough.
Red Flag: CQRS requires event sourcing and separate databases." — That's the advanced
form. Simple CQRS is just separate read/write models.
Follow-up: How would you implement CQRS in a Vertical Slice Architecture project?
src/
Domain/ (entities, value objects)
Application/ (use cases, interfaces)
Infrastructure/ (EF Core, external services)
API/ (controllers, endpoints)
Every feature touches all four projects. A single "Create Product" feature changes files in 4
different directories.
Vertical Slice Architecture organizes by feature:
src/
Features/
Products/
[Link] (endpoint + handler + validator + DTO, all in one file)
[Link]
[Link]
Orders/
[Link]
[Link]
Each file is self-contained — the endpoint, handler, validation, and DTO for one operation.
Key differences:
Coupling Low between layers, high within Low between features, high within
Shared code Forced abstractions (repositories, services) Share when natural, not by default
Onboarding Understand the layers first Open one feature file, understand it
My preference: Vertical Slice for Web APIs. Clean Architecture when the domain is genuinely
complex (DDD, complex business rules). Most CRUD APIs don't need Clean Architecture.
Red Flag: Clean Architecture is always better because it separates concerns." — Separation
by layer isn't always the right separation.
[Link]("OrderService") CSHARP
.AddStandardResilienceHandler(); // Includes retry, circuit breaker, timeout
The standard resilience handler includes: 1. Retry — 3 attempts with exponential backoff for
transient failures (5xx, timeouts) 2. Circuit breaker — after 10 failures in 30 seconds, stop calling
the service for 30 seconds (fail fast instead of waiting) 3. Timeout — 30 second total timeout, 10
second per-attempt timeout
For critical paths, I'd also add:
Fallback — return cached/default data when the service is down:
Red Flag: I'd just retry the request in a loop." — No backoff, no circuit breaking, you'll DDoS
the failing service.
Follow-up: What's the difference between retry and hedging? When would you use hedging?
src/
Modules/
Products/ (own DbContext, own endpoints, internal event bus)
Orders/ (own DbContext, communicates via events)
Inventory/ (own DbContext)
SharedKernel/ (domain events, common types)
My experience: I've seen more projects fail from premature microservices than from staying
monolithic too long. The operational complexity of microservices is massive — distributed
transactions, eventual consistency, network failures, deployment orchestration.
Red Flag: Always microservices because they scale better." — Netflix needed microservices.
Your CRUD API probably doesn't.
The Outbox Pattern. Never publish an event directly — you'll have the dual-write problem where
the database saves but the message broker publish fails (or vice versa).
The pattern: 1. Within the same database transaction, save the order AND the outbox event:
[Link](order);
[Link](new OutboxMessage
{
Id = [Link](),
Type = "OrderPlaced",
Payload = [Link](new OrderPlacedEvent([Link])),
CreatedAt = [Link],
ProcessedAt = null
});
await [Link]();
await [Link]();
1. 2. A background worker polls the outbox table and publishes events to the message broker
(RabbitMQ, Azure Service Bus):
Why not just publish directly? If the database save succeeds but the broker publish fails → you
have an order but no event. If the broker publish succeeds but the database save fails → you
have an event but no order. The outbox makes it atomic.
Libraries like Wolverine and MassTransit have built-in outbox support.
Red Flag: I'd publish the event after saving to the database." — Dual-write problem.
Use BackgroundService for: - Non-critical work (sending emails, updating caches) - Work that's
OK to retry from scratch if the app restarts - Simple periodic tasks (cleanup jobs, health
monitoring)
CSHARP
public class EmailBackgroundService(Channel<EmailRequest> channel) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var request in [Link](ct))
{
await SendEmailAsync(request);
}
}
}
Use a message queue for: - Critical work that MUST complete (payment processing, order
fulfillment) - Work that needs to survive deployments/crashes - Work that needs to scale across
multiple workers - Cross-service communication
My approach: Start with BackgroundService + Channel<T> for simple cases. Move to a
message queue when reliability or scale demands it.
Red Flag: I use [Link]() for background work." — Fire-and-forget with no error handling
or lifecycle management.
The critical security rule: Never trust client-side tenant identification alone. Validate that the
authenticated user belongs to the claimed tenant. Otherwise, a user can access another tenant's
data by changing the header.
Red Flag: I'd add a WHERE clause to every query." — Error-prone. Someone will forget.
The Mediator pattern decouples the sender of a request from its handler. In .NET, MediatR is the
popular library:
Why people love it: - Cross-cutting concerns via pipeline behaviors (logging, validation, caching
applied once) - Clean separation — endpoints are thin, handlers contain logic - Easy to add
behaviors without modifying existing code
Why people avoid it: - Indirection — Ctrl+Click on [Link]() takes you to MediatR,
not your handler. Navigation is harder. - Service locator smell — everything goes through one
interface. You lose explicit dependencies. - Overhead for simple apps — for a CRUD API with 5
endpoints, it's ceremony without benefit.
My take: I don't use MediatR anymore. Wolverine handles messaging and mediation better, or I
just use plain handler classes:
Red Flag: MediatR is required for clean architecture." — It's not. It's a tool, not an architectural
requirement.
Follow-up: How do pipeline behaviors in MediatR compare to endpoint filters in minimal APIs?
[Link](); CSHARP
{ JSON
"FeatureManagement": {
"NewRecommendationEngine": {
"EnabledFor": [
{ "Name": "Percentage", "Parameters": { "Value": 25 } }
]
}
}
}
For dynamic flags (change without redeployment): - Use Azure App Configuration with
IOptionsSnapshot<T> style refresh - Or store flags in a database with a short cache TTL - Or
use a dedicated service (LaunchDarkly, Unleash)
Critical rule: Always clean up old feature flags. A codebase with 50 stale flags is worse than no
flags at all. I add a comment with the flag's expiry date.
Red Flag: I use #if DEBUG for features." — Compile-time flags can't be changed in
production.
Follow-up: How do you test both paths of a feature flag in your integration tests?
[Fact]
public async Task CreateProduct_ReturnsCreated()
{
// Arrange
var request = new { Name = "Test", Price = 99.99 };
// Act
var response = await _client.PostAsJsonAsync("/api/products", request);
// Assert
[Link]().Be([Link]);
var product = await [Link]<ProductResponse>();
product!.[Link]().Be("Test");
}
}
The fixture:
Why this approach: - Real database (Testcontainers) catches migration and query issues that
SQLite/InMemory miss - WireMock for external services gives deterministic responses -
WebApplicationFactory tests the full pipeline (middleware, filters, serialization) - Tests run in CI
without external dependencies
Red Flag: I mock the database with InMemory provider." — InMemory doesn't support
transactions, constraints, or SQL-specific features.
Follow-up: How do you handle test data setup and cleanup between tests?
Why interviewers ask this: Test strategy. Tests whether someone over-tests or under-tests.
GREAT ANSWER
My distribution for a typical Web API: - 70% integration tests — test from HTTP request to
database and back - 20% unit tests — domain logic, validators, complex calculations - 10%
architecture tests — verify conventions (all endpoints have auth, all DTOs are records, etc.)
What I DON'T unit test: - EF Core queries — they generate SQL. Unit testing LINQ that doesn't
translate to SQL is testing the wrong thing. - Controllers/endpoints — the routing, binding, and
serialization are framework code. Test them via integration tests. - DTOs and mapping — if
mapping is trivial, an integration test covers it implicitly.
What I DO unit test:
Red Flag: I unit test everything, including database queries with the InMemory provider." —
False confidence. InMemory behaves differently than real databases.
Follow-up: How do you decide if something is worth testing? What's your cost-benefit
analysis?
I create a test authentication handler that bypasses JWT validation but still sets up the
ClaimsPrincipal:
// Test unauthorized
// Don't add the header — handler returns NoResult
[Fact] CSHARP
public async Task AdminEndpoint_WithoutAuth_Returns401()
{
var client = _factory.CreateClient(); // No test auth
Red Flag: I remove [Authorize] in test environment." — Then you're not testing authorization
at all.
Follow-up: How would you test that a user can only access their own resources (resource-
based authorization)?
[Fact]
public async Task AllMigrations_ApplySuccessfully()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_postgres.GetConnectionString())
.Options;
[Fact]
public async Task Migrations_AreIdempotent()
{
// Apply migrations twice — should not throw
await using var context = CreateContext();
await [Link]();
await [Link](); // Second apply should be no-op
}
[Fact]
public async Task PendingModelChanges_DoNotExist()
{
// Verify no model changes are missing a migration
await using var context = CreateContext();
await [Link]();
var pending = [Link]();
[Link]().BeEmpty();
}
}
Red Flag: I test migrations by running them in the staging environment." — Too late. Test them
in CI.
Follow-up: How do you test a migration that involves data transformation — not just schema
changes?
Why interviewers ask this: Modern testing approach that reduces test maintenance.
GREAT ANSWER
Snapshot testing captures the output of a test and saves it to a file. On subsequent runs, it
compares the output against the saved snapshot. If they differ, the test fails — and you review
the diff.
Using the Verify library:
[Fact] CSHARP
public async Task GetProduct_ReturnsExpectedShape()
{
var response = await _client.GetAsync("/api/products/1");
var json = await [Link]();
{ JSON
"id": 1,
"name": "Widget",
"price": 29.99,
"category": "Electronics",
"createdAt": "DateTimeOffset_1" // Verify scrubs non-deterministic values
}
When it's useful: - API contract testing — catch unintended response shape changes -
Serialization testing — verify JSON output matches expected format - Complex object
comparison — instead of 20 assertions, one snapshot - OpenAPI document testing — snapshot
the generated OpenAPI spec
When it's NOT useful: - Tests where the output changes frequently (timestamps, random IDs) -
Simple assertions where explicit checks are clearer
Verify scrubs non-deterministic values automatically (DateTimes, Guids). You can also add
custom scrubbers.
Red Flag: I've never heard of snapshot testing." — Fair for juniors, concerning for seniors.
Follow-up: How do you handle snapshot updates when the change is intentional? What's the
workflow?
Why interviewers ask this: Fundamental testing structure. Shows if someone writes organized tests.
GREAT ANSWER
[Fact] CSHARP
public async Task CreateProduct_WithValidData_ReturnsCreated()
{
// Arrange — set up the test data and preconditions
var request = new CreateProductRequest("Widget", 29.99m, "Electronics");
Why it matters: - Readability — anyone can understand the test's intent at a glance - One action
per test — the Act section should be one or two lines. If it's more, you're testing too many things
- Clear failure diagnosis — when a test fails, you know which phase went wrong
Common violations: - Asserting in the Arrange phase (checking preconditions that should be
guaranteed) - Multiple Act phases in one test (testing two operations — split into two tests) - No
clear separation (everything mashed together)
Red Flag: I just write code and check if it works at the end." — Unstructured tests are hard to
maintain.
Follow-up: When would you deviate from AAA? Are there cases where a different structure
makes more sense?
CSHARP
private static readonly Respawner _respawner = [Link](connectionString).R
esult;
4. Reduce unnecessary work: - Don't seed test data globally — each test creates only what it
needs - Use WebApplicationFactory once per test collection, not per test - Disable logging in
test configuration
5. Split into fast and slow suites: - Fast suite (< 2 min): core business logic, critical paths — runs
on every commit - Full suite (15 min): everything — runs on PR merge
Red Flag: Switch to unit tests." — That changes what you're testing, not the speed.
Never call the real payment gateway in tests. Use WireMock to create a fake HTTP server:
[Fact]
public async Task ProcessPayment_Success_ReturnsOk()
{
// Arrange — configure WireMock to return a successful response
_paymentApi.Given(
[Link]()
.WithPath("/api/charges")
.WithBody(new JsonMatcher(new { amount = 100, currency = "USD" }))
.UsingPost())
.RespondWith(
[Link]()
.WithStatusCode(200)
.WithBody("""{"chargeId": "ch_123", "status": "succeeded"}"""));
// Act
var response = await _client.PostAsJsonAsync("/api/orders/1/pay",
new { Amount = 100 });
// Assert
[Link]().Be([Link]);
}
[Fact]
public async Task ProcessPayment_GatewayTimeout_ReturnsServiceUnavailable()
{
// Test failure scenario
_paymentApi.Given([Link]().WithPath("/api/charges").UsingPost())
.RespondWith([Link]().WithDelay([Link](30)));
[Link]().Be([Link]);
}
Why WireMock over mocking IHttpClient: - Tests the full HTTP pipeline (serialization, headers,
error handling) - You can test timeouts, slow responses, and network errors - The test reads like
a real scenario, not implementation details
Red Flag: I mock the HttpClient with Moq." — You can't mock HttpClient directly (it's a
concrete class), and mocking HttpMessageHandler tests implementation details.
Why interviewers ask this: Shows mature engineering practices beyond just feature testing.
GREAT ANSWER
Architecture tests verify that your code follows structural conventions automatically. They catch
violations in CI instead of code review:
[Fact]
public void Endpoints_ShouldNot_DependOnInfrastructure()
{
[Link](typeof(Program).Assembly)
.That().ResideInNamespace("[Link]")
.ShouldNot().HaveDependencyOn("Infrastructure")
.GetResult().[Link]().BeTrue();
}
[Fact]
public void AllEndpoints_ShouldRequireAuthorization()
{
var endpoints = [Link](typeof(Program).Assembly)
.That().ImplementInterface(typeof(IEndpoint))
.GetTypes();
[Fact]
public void DomainLayer_ShouldNotReference_EntityFramework()
{
[Link](typeof(Order).Assembly)
.ShouldNot().HaveDependencyOn("[Link]")
.GetResult().[Link]().BeTrue();
}
What I test architecturally: - Layer dependency rules (domain doesn't reference infrastructure) -
Naming conventions (handlers end with "Handler", DTOs end with "Request"/"Response") -
Security (all endpoints have authorization attributes) - EF Core (all entities have configurations in
IEntityTypeConfiguration<T> )
Red Flag: We enforce architecture in code reviews." — Manual review misses things.
Automated tests don't.
xUnit v3 is my default: - Most popular in the .NET ecosystem - Constructor injection for test
fixtures (DI-friendly) - Parallel test execution by default - Clean, convention-based (no
[TestClass] attributes)
FluentAssertions gives much better error messages too. Instead of "Expected True, got False",
you get "Expected [Link] to be positive, but found -5.00".
Other tools in my test stack: - Testcontainers — real databases in Docker for integration tests -
WireMock — fake HTTP servers for external API testing - Bogus — generate realistic test data -
Verify — snapshot testing - Respawn — fast database cleanup between tests
Red Flag: NUnit because that's what I learned." — Not wrong, but shows lack of ecosystem
awareness. xUnit is the standard for new .NET projects.
String-based logging:
Structured logging:
Key practices: 1. Use message templates, not string interpolation — templates create
searchable properties 2. Enrich with context — correlation ID, user ID, tenant ID on every log
entry 3. One request log line — Serilog's UseSerilogRequestLogging replaces the noisy default
logging 4. Log levels matter — Information for business events, Warning for recoverable
issues, Error for failures 5. Never log sensitive data — mask PII, don't log request bodies with
passwords
Red Flag: I use [Link] for debugging." — Not queryable, not structured, not
production-ready.
Why interviewers ask this: Health checks are how load balancers and orchestrators know if your service
is healthy.
GREAT ANSWER
[Link]() CSHARP
.AddDbContextCheck<AppDbContext>("database") // Can we reach the DB?
.AddRedis(redisConnectionString, "redis") // Can we reach Redis?
.AddUrlGroup(new Uri("[Link]
"payment-gateway"); // Is the payment API up?
Red Flag: I just check if the app returns 200 on any endpoint." — That tells you the app is
running, not that it can serve traffic.
Follow-up: How do you configure Kubernetes liveness and readiness probes for a .NET API?
# Runtime stage
FROM [Link]/dotnet/aspnet:10.0-noble-chiseled AS runtime
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "[Link]"]
Key decisions:
1. 1. Multi-stage build — the SDK image is ~700MB. The runtime image is ~100MB. The chiseled
image is ~30MB. Only ship what you need.
1. 2. Chiseled images ( -noble-chiseled ) — distroless Ubuntu. No shell, no package manager,
no attack surface. Perfect for APIs.
1. 3. Non-root user ( USER $APP_UID ) — never run as root in production.
1. 4. Layer caching — COPY *.csproj and dotnet restore before copying source code.
NuGet packages are cached unless the .csproj changes.
1. 5. Port 8080 — .NET 8+ defaults to port 8080 in containers (not 80).
.dockerignore — exclude bin/ , obj/ , .git/ , and test projects from the build context.
Alternative — no Dockerfile at all:
.NET SDK can generate container images natively. Great for simple apps.
Red Flag: I copy the entire solution into the container and run dotnet run." — Development
mode, not optimized, includes SDK.
When Kubernetes terminates a pod, it sends SIGTERM . The app needs to: 1. Stop accepting new
requests 2. Finish processing in-flight requests 3. Clean up resources (close DB connections,
flush logs) 4. Exit
.NET handles this via IHostApplicationLifetime :
[Link](() =>
{
[Link]("Shutdown complete");
[Link](); // Flush Serilog
});
Kubernetes configuration:
spec: YAML
terminationGracePeriodSeconds: 30 # Time before SIGKILL
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["sleep", "5"] # Allow load balancer to remove the pod
The critical detail: There's a race condition between Kubernetes removing the pod from the
service (load balancer) and sending SIGTERM. The preStop sleep ensures the load balancer
stops sending traffic BEFORE the app starts shutting down.
Configure shutdown timeout in .NET:
Red Flag: The app just stops when Kubernetes kills it." — In-flight requests get 502 errors.
Why interviewers ask this: DevOps maturity. Most developers need CI/CD knowledge.
GREAT ANSWER
jobs:
build-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore -c Release
- name: Test
run: dotnet test --no-build -c Release --logger "trx"
env:
ConnectionStrings__Default: "Host=localhost;Database=test;Username=postgres;Pa
ssword=test"
- name: Publish
if: [Link] == 'refs/heads/main'
run: dotnet publish src/Api -c Release -o ./publish
Key practices: 1. Services block for database — real PostgreSQL for integration tests 2. Build
once, test once, publish once — don't rebuild between steps 3. Only deploy from main — PR
branches get build + test, not deploy 4. Tag with commit SHA — every image is traceable to a
commit 5. Separate deploy step — build and deploy are different jobs. Deploy requires manual
approval for production.
Red Flag: We deploy by SSH-ing into the server and running git pull." — No CI, no tests, no
rollback capability.
[Link]() CSHARP
.WithMetrics(metrics =>
{
[Link]()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter();
})
.WithTracing(tracing =>
{
[Link]()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter();
});
// In handler
[Link](1, new KeyValuePair<string, object?>("payment_method", "stripe"));
Red Flag: We check the logs when users report issues." — Reactive, not proactive. You should
know about problems before users do.
Follow-up: How do distributed traces help you debug a slow request that touches multiple
services?
When to use which: - ILogger<T> — 99% of the time. Standard approach for service classes. -
ILoggerFactory — when you need dynamic categories (background job processors, plugin
systems, generic classes where T isn't meaningful).
Don't inject ILogger (non-generic) — it uses an empty category. Your logs lose context about
which class generated them.
Red Flag: I inject ILogger without the type parameter." — Loses category context.
Follow-up: How do log scopes work and when would you use [Link]()?
[Link] (base)
↓ overrides
appsettings.{Environment}.json (environment-specific)
↓ overrides
Environment variables
↓ overrides
User secrets (dev only)
↓ overrides
Command-line args
My approach:
[Link] — defaults that work everywhere:
{ JSON
"Logging": { "LogLevel": { "Default": "Information" } },
"ConnectionStrings": { "Default": "" },
"Cache": { "ExpirationMinutes": 30 }
}
{ JSON
"Logging": { "LogLevel": { "Default": "Debug" } },
"ConnectionStrings": { "Default": "Host=localhost;Database=myapp_dev" }
}
Production secrets — NEVER in JSON files. Use environment variables or a secret manager:
[Link]<DatabaseOptions>() CSHARP
.BindConfiguration("Database")
.ValidateDataAnnotations()
.ValidateOnStart(); // Fail fast if config is missing
Red Flag: I have different [Link] files and deploy the right one." — That's manual
and error-prone.
Why interviewers ask this: Entry point understanding. Tests if someone knows how an API boots up.
GREAT ANSWER
[Link] is the application's entry point. It configures everything the API needs:
[Link]();
Two phases: 1. builder phase — register services, configure logging, add authentication
schemes. Nothing is running yet. 2. app phase — configure middleware pipeline and map
endpoints. The order of middleware registration matters.
Since .NET 6: No more [Link] . Everything is in [Link] with top-level statements. The
partial class Program is implicit — useful for WebApplicationFactory in tests:
Red Flag: It's where the Main method is." — Technically true but shows no understanding of
what it configures.
Follow-up: How would you organize [Link] as it grows beyond 100 lines? Extension
methods?
az webapp deployment slot swap --slot staging --target-slot production # Swap back BASH
Database migrations — the tricky part: - If the new version added columns but didn't remove
any → rollback is safe, old code ignores new columns - If the migration dropped or renamed
columns → you need a forward-fix migration, not a rollback - This is why I always make
migrations backward-compatible: add new columns first, deploy new code, then remove old
columns in a later release
Prevention: - Canary deployments — route 5% of traffic to the new version first - Feature flags
— deploy the code but keep new features disabled - Blue-green deployments — run both
versions, switch traffic atomically
Post-incident: Write a blameless post-mortem. What happened, why the tests didn't catch it,
how to prevent it.
Red Flag: We fix forward — we never rollback." — Sometimes rollback is the fastest way to
stop the bleeding.
Primary constructors let you declare constructor parameters directly on the class:
Red Flag: I haven't upgraded to C# 12 yet." — C# 12 shipped with .NET 8. It's been available
for 2+ years.
Follow-up: How do primary constructors interact with inheritance? Can a derived class use
the base class's primary constructor parameters?
When I use records: - DTOs (request/response models) — immutable, value equality for testing:
var updated = product with { Price = 39.99m }; // Creates a new instance CSHARP
record class vs record struct : - record class (default) — reference type, heap allocated -
record struct — value type, stack allocated, good for small DTOs in hot paths
Red Flag: Records are the same as classes but shorter." — Misses value equality, the key
differentiator.
Follow-up: How do records interact with JSON serialization? Any gotchas with
[Link]?
// Before CSHARP
return [Link]<Product>();
return [Link]<Product>();
// After
return []; // Compiler picks the most efficient representation
Red Flag: I still use new List<int> { ... }." — Not wrong, but shows unfamiliarity with
modern C#.
Span<T> is a stack-allocated view over contiguous memory (arrays, strings, native memory). It
avoids allocations by slicing existing memory instead of copying:
Red Flag: I use Span everywhere for performance." — Premature optimization. Most API
bottlenecks are I/O, not allocations.
Follow-up: Can you use Span<T> in async methods? Why or why not?
Why interviewers ask this: Modern C# feature used for clean control flow.
GREAT ANSWER
Pattern matching lets you test values against patterns and extract data:
// Relational pattern
string GetPriceCategory(decimal price) => price switch
{
< 10 => "Budget",
>= 10 and < 50 => "Mid-range",
>= 50 and < 200 => "Premium",
_ => "Luxury"
};
Red Flag: I use is for null checks only." — Missing the power of pattern matching.
Follow-up: What's the switch expression vs the switch statement? When would you use
each?
In my API projects:
- class — EF Core entities (need mutability for change tracking), services, handlers
public class Product { public int Id { get; set; } public string Name { get; set; }CSHARP
}
- struct — small value types in hot paths (coordinates, money, strongly-typed IDs)
When struct wins: When you're creating millions of small objects and want to avoid GC
pressure. For typical API work, class and record are fine — don't micro-optimize.
Red Flag: I use classes for everything." — Missing records for DTOs is a lot of unnecessary
boilerplate.
Follow-up: What does readonly record struct give you over record struct?
Why interviewers ask this: Streaming data handling. Modern async pattern.
GREAT ANSWER
IAsyncEnumerable<T> returns items one at a time as they become available, instead of loading
everything into memory:
When to use:
1. 1. Large dataset exports (CSV, JSON streaming):
Red Flag: I always use ToListAsync() because it's simpler." — Fine for small datasets,
problematic for large ones.
Source generators produce C# code at compile time, eliminating runtime reflection. Three key
uses in APIs:
1. [Link] source generation — faster serialization:
[JsonSerializable(typeof(ProductResponse))] CSHARP
[JsonSerializable(typeof(List<ProductResponse>))]
internal partial class AppJsonContext : JsonSerializerContext;
[GeneratedRegex(@"^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$")] CSHARP
private static partial Regex EmailRegex();
Why it matters: - AOT (Ahead of Time) compilation — source generators make your code AOT-
compatible by removing reflection - Startup performance — no reflection-based initialization -
Trimming — the linker can remove unused code because dependencies are explicit
Red Flag: Source generators are only for library authors." — They're increasingly important for
application developers, especially with AOT.
Follow-up: What's the difference between source generators and Roslyn analyzers?
Minimal APIs define endpoints directly in [Link] (or extension methods) without the
ceremony of controller classes:
// Equivalent Controller
[ApiController]
[Route("api/products")]
public class ProductsController(AppDbContext db) : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await [Link](id);
return product is null ? NotFound() : Ok(product);
}
}
Parameter
Explicit Convention-based (implicit)
binding
When I'd still use controllers: - Large teams familiar with MVC patterns - When you need model
binding features that minimal APIs lack - Migration of existing controller-based projects (don't
rewrite just to rewrite)
Follow-up: How do you organize minimal APIs as the project grows? What's the MapGroup
approach?
Red Flag: I'm still on .NET 6 LTS." — .NET 6 is end of life. Staying current matters for security
and features.
Follow-up: How do you plan a .NET version upgrade for a production application? What's your
migration checklist?
Why interviewers ask this: Testability of time-dependent code. A practical modern .NET feature.
GREAT ANSWER
// Registration
[Link]([Link]); // Real time in production
[Fact] CSHARP
public void IsBlackFriday_OnNovember25_ReturnsTrue()
{
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 11, 25, 0, 0, 0, TimeSp
[Link]));
var service = new DiscountService(fakeTime);
[Link]().Should().BeTrue();
}
[Fact]
public void IsBlackFriday_OnDecember1_ReturnsFalse()
{
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 12, 1, 0, 0, 0, TimeSpa
[Link]));
var service = new DiscountService(fakeTime);
[Link]().Should().BeFalse();
}
Why this matters: Before TimeProvider , people used [Link] directly, making tests
non-deterministic (tests pass on Black Friday, fail on other days), or they built custom IClock
interfaces.
TimeProvider also provides CreateTimer() for testable timers and GetTimestamp() for
testable performance measurement.
The field keyword gives you access to the auto-generated backing field inside a property,
without manually declaring it:
// After (C# 14) — field keyword references the compiler-generated backing field
public string Name
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
1. 2. Validation in properties:
Why it matters: Eliminates the boilerplate of declaring backing fields for properties that need
custom logic in the getter or setter, while still using auto-property syntax.
Red Flag: I haven't heard of the field keyword." — Fair, it's brand new in C# 14 / .NET 10. But
senior devs should follow language previews.
Follow-up: How does field interact with required and init property accessors?
[Link]/newsletter
Free Resources