ASP.NET Core Web API & REST
In short: REST is an architectural style for networked APIs (Roy Fielding, 2000).
- Constraints: client–server separation, statelessness (each request carries everything needed, e.g. a token), cacheability, a uniform interface (resources identified by URIs, standard methods, self-descriptive messages, optionally hypermedia), a layered system, and optional code-on-demand.
- In practice: resource-oriented URLs, HTTP verbs, status codes, and JSON representations.
In short: POST creates a resource (server assigns the ID) or triggers a process; not idempotent; returns 201 + Location.
- PUT replaces the whole resource at a known URI; idempotent (sending it twice gives the same state).
- PATCH partially updates (JSON Patch or JSON Merge Patch); not necessarily idempotent.
- Missing fields in PUT usually mean "set to default/null", in PATCH they mean "leave unchanged".
In short: 401 Unauthorized really means unauthenticated: no valid credentials, so the client should log in or refresh its token (the response includes WWW-Authenticate).
- 403 Forbidden means the identity is known but lacks permission, and re-authenticating won't help.
- In ASP.NET Core,
Challenge()produces 401 andForbid()produces 403.
[ApiController] attribute do?In short: It enables API conventions: attribute routing is required, invalid ModelState automatically returns 400 with ValidationProblemDetails (no if (!ModelState.IsValid) needed), binding sources are inferred ([FromBody] for complex types, route/query for simple ones, IFormFile from the form), and error status codes get ProblemDetails bodies.
- Apply it to the class or the whole assembly.
In short: Middleware are components in the request pipeline.
- Each receives
HttpContext, can do work, callnextto pass the request on, and do more work on the way back, or short-circuit. - Write a class with a constructor taking
RequestDelegate nextand anInvokeAsync(HttpContext)method, then register it withapp.UseMiddleware<T>(); - or use inline
app.Use(async (ctx, next) => ...). Runis terminal;Mapbranches.- Order defines behaviour.
IActionResult and ActionResult<T>?In short: IActionResult can return any result but carries no type information.
ActionResult<T>(2.1+) lets you return either aT(implicitly wrapped in a 200 OK) or anyActionResult(NotFound()), and lets OpenAPI tools infer the response type.- Minimal APIs use
TypedResultsandResults<Ok<T>, NotFound>for the same purpose.
In short: Both are fully supported and equally fast.
- Controllers suit large APIs with many endpoints, teams familiar with MVC, and heavy use of MVC filters and conventions.
- Minimal APIs suit microservices and small services;
- they have less ceremony, route groups, endpoint filters, typed results and Native AOT support (controllers aren't AOT-compatible).
- They can coexist in one app.
- Organise Minimal APIs with extension methods per feature (
app.MapOrders()).
In short: The client authenticates with an identity provider (Microsoft Entra ID, Auth0, Duende IdentityServer, or your own login endpoint) and receives a signed JWT access token.
- It sends
Authorization: Bearer <token>on each request. - The JwtBearer handler validates the signature (via the authority's published keys or a configured key), issuer, audience and expiry, then builds
HttpContext.Userfrom the claims. [Authorize]with roles, scopes or policies decides access.- Tokens are stateless, so revocation needs short expiry plus refresh tokens or a deny list.
In short: Browsers enforce the same-origin policy: a page from https://app.com can't read responses from https://api.com unless the API sends Access-Control-Allow-Origin (and related headers).
- For non-simple requests, the browser first sends a preflight
OPTIONS. - CORS errors mean the server didn't allow that origin, method or header, or
UseCorsis misplaced in the pipeline. - Fix server-side with a named policy listing the exact origins.
- CORS doesn't protect the API from non-browser clients.
In short: Creating and disposing HttpClient per request leaves sockets in TIME_WAIT and can exhaust ports.
- A single long-lived static instance doesn't respect DNS changes.
IHttpClientFactorymanages pooled handlers with a rotation lifetime, supports named and typed clients, central configuration, and delegating handlers for logging, auth headers and resilience (AddStandardResilienceHandler).
In short: Use UseExceptionHandler() with IExceptionHandler implementations (.NET 8+) and AddProblemDetails() to return RFC 9457 ProblemDetails JSON.
- Map known exception types to statuses (NotFound → 404, Validation → 400, Conflict → 409), log unexpected ones as errors with a trace ID, and never return stack traces in production.
- For expected failures, consider the Result pattern instead of exceptions.
In short: Use the Asp.Versioning packages.
- Strategies: URL segment (
/api/v2/orders, most common and cache-friendly), query string (?api-version=2.0), header (api-version), or media type. - Keep old versions running for a deprecation period, report supported/deprecated versions in headers, document each version in OpenAPI, and introduce a new major version only for breaking changes (removing or renaming fields, changing types or semantics).
In short: The client states the formats it accepts (Accept: application/json or application/xml), and the server picks an output formatter that matches.
- If none matches, it uses the default (JSON) or returns 406 if
ReturnHttpNotAcceptable = true. - Input format is determined by
Content-Type; an unsupported one gives 415. - XML support is added with
AddXmlSerializerFormatters().
In short: Offset: Skip((page-1)*size).Take(size) plus a total count.
- It's simple and allows jumping to any page, but it gets slow for deep pages (the DB still scans skipped rows) and is unstable when data changes.
- Keyset/cursor:
WHERE (CreatedAt, Id) < (@lastCreatedAt, @lastId) ORDER BY CreatedAt DESC, Id DESCwithTake(size)and anextCursorin the response. - It's fast and stable, suited to infinite scroll and large tables.
- Always cap the page size.
In short: Built-in since .NET 7 (AddRateLimiter): Fixed window (N requests per window;
- bursty at window edges), Sliding window (window split into segments;
- smoother), Token bucket (tokens refill at a rate;
- allows bursts up to bucket size), Concurrency (max simultaneous requests).
- Partition by user, API key or IP; return 429 with
Retry-After. - For multiple instances, apply limits at the gateway (APIM, Front Door) or use a distributed store.
In short: Require an Idempotency-Key header (a client-generated UUID).
- On the first request, store the key with request hash, status and response in a table (unique constraint on key + client).
- Concurrent duplicates hit the constraint and wait or get 409.
- Later repeats return the stored response instead of re-executing.
- Expire keys after a period.
- Combine this with DB unique constraints on business identifiers.
- Stripe's API popularised the pattern.
In short: HTTPS only with HSTS;
- OAuth 2.0/OIDC with short-lived JWTs (Authorization Code + PKCE for user clients, client credentials for services);
- scope/role/policy authorisation plus resource ownership checks (IDOR);
- input validation and request size limits;
- parameterised data access;
- rate limiting and an API gateway/WAF (APIM, Front Door);
- strict CORS;
- secrets in Key Vault with managed identities;
- security headers;
- dependency scanning;
- logging and alerting on auth failures;
- no sensitive data in URLs or logs;
- and regular penetration tests or OWASP ZAP scans.
In short: gRPC uses HTTP/2 with Protobuf binary serialisation, has contract-first .proto definitions with generated strongly typed clients, supports bidirectional streaming, and has lower latency and smaller payloads.
- Use it for internal microservice-to-microservice communication, streaming or polyglot environments.
- Use REST/JSON for public APIs and browser clients (gRPC needs gRPC-Web in browsers), and where human readability and HTTP caching matter.
In short: Unit tests for services and handlers with mocked dependencies.
- Integration tests with
WebApplicationFactory<Program>: spin up the real pipeline in memory, replace infrastructure with Testcontainers SQL Server or a test DB, call endpoints withHttpClient, and assert status codes and JSON. - Add auth tests (fake JWT or a test auth handler), contract tests (OpenAPI diff, Pact), and load tests (k6, Azure Load Testing) before release.
{ "success": false, "error": "Not found" } for everything. How would you improve it?In short: Use proper HTTP semantics.
- Return 404 for not found, 400/422 with ValidationProblemDetails for invalid input, 409 for conflicts, 401/403 for auth, 201 + Location for creates, 204 for deletes, and 500 ProblemDetails with a traceId for unexpected errors.
- Clients, proxies, caches and monitoring tools rely on status codes; with 200 for everything, error-rate dashboards and retries break.
- Roll it out in a new API version so existing clients keep working.
In short: Profile first (Application Insights, EF logging).
- Typical fixes: add pagination (keyset for deep lists), project only needed columns with
Selectinto DTOs, useAsNoTracking, add a covering index on(CustomerId, CreatedAt DESC) INCLUDE (Total, Status), avoid N+1 by not lazy loading, return totals separately or approximately, and cache hot results with HybridCache. - If the client needs everything for export, make it an async job (202 Accepted + status URL) that streams a file.
In short: Add timeouts (per attempt and total), retries with exponential backoff and jitter only for idempotent calls (or with idempotency keys), a circuit breaker to fail fast while it's down, and a bulkhead/concurrency limit so payment calls can't consume all resources.
AddStandardResilienceHandler()covers most of this.- For non-interactive flows, put requests on a queue (Service Bus) and process them asynchronously with status callbacks or webhooks.
- Return a clear 503/202 to clients and alert on circuit-open events.
In short: Treat the API as a contract.
- Make only additive changes in the current version (new optional fields; never rename or remove).
- Introduce a new version (
/v2) for breaking changes, and keep v1 alive with a published deprecation timeline (Deprecation/Sunsetheaders). - Add OpenAPI diff checks in CI to catch breaking changes, and use consumer-driven contract tests.
- Clients should ignore unknown fields (tolerant reader).
In short: The state was per instance: in-memory session or cache, and maybe Data Protection keys (used for cookies and antiforgery) generated per instance.
- Fix: keep the API stateless (JWT), store session/cache in Redis (
IDistributedCache/HybridCache L2), and persist Data Protection keys to a shared store (Azure Blob + Key Vault, Redis or a DB) with the same application name. - Avoid sticky sessions as a crutch.
In short: OpenAPI is a standard, machine-readable description of an HTTP API (endpoints, parameters, schemas, auth);
- Swagger is the original name and the tooling around it (Swagger UI to explore and try endpoints).
- In .NET 9+ use the built-in
builder.Services.AddOpenApi()+app.MapOpenApi()(serves/openapi/v1.json) and add a UI such as Scalar or Swagger UI; - earlier versions use Swashbuckle (
AddSwaggerGen,UseSwagger,UseSwaggerUI) or NSwag. - The document also lets you generate typed clients for Angular/React and test tools.