ASP.NET Core interview questions

25 questions on ASP.NET Core and EF Core: DI lifetimes, the middleware pipeline, change tracking, JWT auth, CORS, model binding, and API pagination, with code.

01ASP.NET Core & EFFoundation

Dependency Injection Lifetimes & Container Rules

Answer

  • The ASP.NET Core built-in IoC container (Microsoft.Extensions.DependencyInjection) manages the instantiation, dependency resolution, and disposal of application services according to three distinct lifetime rules.
  • Transient (AddTransient): A new instance is created every single time it is requested from the service provider. Suitable for lightweight, stateless algorithms or utilities.
  • Scoped (AddScoped): A single instance is created per scope and shared across all consumers within that scope. In ASP.NET Core, an HTTP request represents one scope. Entity Framework Core DbContext is registered as Scoped so that a single database transaction and change-tracker session is shared throughout the HTTP request.
  • Singleton (AddSingleton): A single instance is created on first resolution (or registration) and lives for the entire lifetime of the process. Shared across all concurrent HTTP requests and threads.
Flow diagram: Service lifetimes and scope boundaries
C# (ASP.NET Core DI)
public class BackgroundOrderWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<BackgroundOrderWorker> _logger; public BackgroundOrderWorker(IServiceScopeFactory scopeFactory, ILogger<BackgroundOrderWorker> logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Safe resolution: Explicitly create a scope to avoid captive Scoped dependencies using (var scope = _scopeFactory.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var pendingOrders = await db.Orders.Where(o => o.Status == "Queued").ToListAsync(stoppingToken); // Process orders... } await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } }

Captive Dependencies and Concurrency Hazards

  • The Captive Dependency Bug: Occurs when a service with a longer lifetime captures a service with a shorter lifetime (such as injecting a Scoped service like DbContext into a Singleton service). The Singleton keeps the initial Scoped instance alive forever, bypassing per-request cleanup.
  • Consequences: In the case of DbContext, sharing it across concurrent requests via a Singleton causes multi-threaded access exceptions (DbContext is not thread-safe), memory leaks from an unbounded ChangeTracker, and stale data reads.
  • Built-in Scope Validation: ASP.NET Core automatically enables scope validation in the Development environment (ValidateScopes = true). If a Singleton injects a Scoped dependency, the application throws an InvalidOperationException at startup or on first resolution.
  • Consuming Scoped Services in Background Singletons: When an IHostedService or Singleton requires a Scoped service, inject IServiceScopeFactory, call CreateScope(), resolve dependencies inside a using block, and let the scope dispose cleanly.

Disposal Rules and IDisposable Traps

  • The container tracks every service it creates that implements IDisposable or IAsyncDisposable. When a scope ends (at the end of an HTTP request), the container disposes all tracked Scoped and Transient instances created within that scope.
  • Transient Disposal Leak: If you manually resolve Transient IDisposable services from the root provider (app.Services) instead of a request scope, the root container retains a reference to those instances forever to dispose them at shutdown, creating a slow memory leak.
02ASP.NET Core & EFFoundation

EF Core ChangeTracker: States, Snapshots, and Query Modes

Answer

  • Entity Framework Core ChangeTracker tracks changes to entity instances loaded into memory by a DbContext, allowing SaveChanges() to automatically construct and execute minimal SQL INSERT, UPDATE, and DELETE statements.
  • Snapshot Tracking: By default, when an entity is queried, EF Core stores a snapshot of its original property values in internal ChangeTracker entries. When SaveChanges() is called, DetectChanges() runs a property-by-property comparison between current values and the snapshot.
  • Entity States: An entity in EF Core is in one of five states: Detached (not tracked), Unchanged (matches database), Added (will be inserted), Modified (has property changes to update), or Deleted (will be deleted).
  • AsNoTracking() Optimization: Skips allocating snapshot buffers and identity-map tracking. Read-only API queries should always use AsNoTracking() to reduce heap allocations, lower GC overhead, and speed up query processing.
State diagram: ChangeTracker snapshot evaluation lifecycle
C# (EF Core)
// High-performance read-only query using AsNoTracking and projection public async Task<List<CustomerOrderSummaryDto>> GetCustomerOrdersAsync(int customerId, AppDbContext db, CancellationToken ct) { return await db.Customers.AsNoTracking() .Where(c => c.Id == customerId) .Select(c => new CustomerOrderSummaryDto { CustomerName = c.Name, Orders = c.Orders.Select(o => new OrderDto { Id = o.Id, Total = o.Total }).ToList() }) .ToListAsync(ct); } // Efficient targeted update on disconnected entity without full graph tracking public async Task UpdateOrderStatusAsync(int orderId, OrderStatus newStatus, AppDbContext db, CancellationToken ct) { var order = new Order { Id = orderId }; db.Orders.Attach(order); order.Status = newStatus; db.Entry(order).Property(o => o.Status).IsModified = true; await db.SaveChangesAsync(ct); // Emits: UPDATE Orders SET Status = @p0 WHERE Id = @p1 }

AsSplitQuery vs Cartesian Explosion

  • Single Query (Default): Loading multiple 1-to-many child collections with .Include() generates a single SQL query with multiple LEFT JOINs. This causes a Cartesian explosion of rows returned over the database network wire, degrading query performance and consuming large client memory.
  • Split Queries: Calling .AsSplitQuery() instructs EF Core to generate one separate SQL query for the parent table and one query for each included collection, joining them in application memory via primary keys.
  • Trade-off: Split queries eliminate data duplication over the network, but lack transaction isolation across queries unless executed inside an explicit database transaction.
03ASP.NET Core & EFFoundation

Minimal APIs

Answer

  • Minimal APIs map HTTP endpoints with MapGet, MapPost, and related methods in Program.cs without controller classes.
  • Handlers take parameters from route, query, header, and body via the same binding rules as controllers, plus DI injection.
  • MapGroup prefixes routes and can attach authorization, filters, and OpenAPI tags to a whole set of endpoints.
  • Return TypedResults or Results helpers for status codes that OpenAPI can describe clearly.
  • Endpoint filters run around the handler (validation, logging) similar in spirit to MVC action filters.
  • Good fit for small services and microservices where controllers add little structure. Large apps may still prefer controllers for organization.
  • AddSwaggerGen / OpenAPI documents minimal endpoints when you enable the metadata integrations.
  • Validate input with DataAnnotations filters, built-in validation, or libraries such as FluentValidation wired as filters.
  • Authorization uses the same [Authorize] metadata and RequireAuthorization on groups as the rest of ASP.NET Core.
  • Do not mix opposing styles randomly in one service. Pick controllers or minimal APIs as the default surface and stick to it.
Flow diagram: Minimal API
C#
var api = app.MapGroup("/api/items"); api.MapGet("/{id:int}", async (int id, AppDb db) => await db.Items.FindAsync(id) is { } item ? Results.Ok(item) : Results.NotFound());
04ASP.NET Core & EFFoundation

Kestrel vs IIS Express

Answer

  • Kestrel is the cross-platform web server built into ASP.NET Core. It speaks HTTP/1.1 and HTTP/2 (and HTTP/3 when configured).
  • In production on Windows, IIS often sits as a reverse proxy in front of Kestrel for TLS termination, Windows auth, and process management via ANCM.
  • On Linux, nginx or another reverse proxy often fronts Kestrel for TLS and static concerns the same way.
  • IIS Express is a lightweight developer host that mimics IIS on a workstation. It is not full IIS and is not a production host.
  • dotnet run and many templates host Kestrel directly. Visual Studio can launch IIS Express for F5 debugging when the profile says so.
  • Prefer Kestrel (plus a reverse proxy when you need one) for deployed apps. Use IIS Express only as a local debugging option.
  • launchSettings.json profiles choose applicationUrl, env vars, and whether IIS Express or Project (Kestrel) starts.
  • Kestrel limits (max request body, concurrent connections) are configured in code or config. Misconfigured limits show up as 413 or hung clients.
  • Do not confuse IIS Express quirks (URL ACL, virtual directories) with how Kestrel binds ports in containers.
Flow diagram: Dev host vs production edge
C#
// Typical production: reverse proxy -> Kestrel // launchSettings.json may use Kestrel or IIS Express for local F5 var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "hosted by Kestrel (possibly behind a proxy)"); app.Run();
05ASP.NET Core & EFFoundation

Configuration in ASP.NET Core

Answer

  • Configuration is a layered key/value system: appsettings.json, appsettings.{Environment}.json, env vars, command line, and more.
  • Later providers override earlier ones for the same key. Environment variables commonly override JSON in containers.
  • IConfiguration indexes with "Section:Key" or GetSection. Prefer the Options pattern for typed access.
  • User secrets store local credentials outside source control in Development. They are not for production.
  • Azure Key Vault or similar providers pull secrets at runtime in production without baking them into images.
  • ReloadOnChange can refresh JSON files. IOptionsMonitor observes those changes; IOptions does not.
  • Double show __ in env vars maps to ":" hierarchy (MyApp__ConnectionString).
  • Validate required settings at startup so the process fails fast instead of on the first request.
  • Never log full connection strings or API keys. Configuration dumps in debug middleware are a common leak.
  • Different environments (Development, Staging, Production) select different JSON files via ASPNETCORE_ENVIRONMENT.
Flow diagram: Layered configuration providers
C#
var port = builder.Configuration.GetValue<int>("Smtp:Port"); var smtp = builder.Configuration.GetSection("Smtp"); builder.Services.Configure<SmtpOptions>(smtp); public sealed class SmtpOptions { public string Host { get; set; } = ""; public int Port { get; set; } }
06ASP.NET Core & EFFoundation

Quartz.NET and Hangfire

Answer

  • Hangfire and Quartz.NET both run background work inside .NET processes, often hosted next to an ASP.NET Core app.
  • Hangfire is a durable job queue: enqueue fire-and-forget, delayed, or recurring jobs; persist them; retry; inspect them in a dashboard.
  • Quartz.NET is a scheduler: JobDetail plus Trigger (cron, calendars, simple intervals) with misfire instructions.
  • Quartz clustering uses a shared store so only one node fires a given trigger when multiple nodes run.
  • Pick Hangfire when the app enqueues work from requests (emails, reports) and operators need a UI for retries.
  • Pick Quartz.NET when calendars, cron complexity, and misfire policy are the hard part.
  • Jobs must be idempotent. Neither product gives perfect exactly-once execution across crashes and visibility timeouts.
  • Disable overlapping execution when a job must not run twice at once (DisableConcurrentExecution in Quartz, or equivalent locks).
  • Persist job storage (SQL Server, Redis for Hangfire) so recycle of the web process does not lose scheduled work.
  • Do not run heavy CPU jobs on every web instance without a plan. Prefer a worker process or compete with distributed locks.
Flow diagram: Background jobs
C#
// Hangfire BackgroundJob.Enqueue(() => email.SendWelcome(userId)); RecurringJob.AddOrUpdate("nightly", () => reports.Run(), Cron.Daily); // Quartz.NET (sketch): register IJob + cron trigger in the scheduler
07ASP.NET Core & EFFoundation

IActionResult vs ActionResult<T>

Answer

  • IActionResult is the MVC/Web API contract for an executable result: OkObjectResult, NotFoundResult, BadRequestObjectResult, and others.
  • Returning IActionResult lets one action branch across status codes without fixing a single body type.
  • ActionResult<T> implements IActionResult and also carries T for OpenAPI / ApiExplorer as the success body type.
  • You can return T directly from ActionResult<T>; the framework wraps it as 200 OK with that payload.
  • You can still return NotFound() or BadRequest() from ActionResult<T> when the happy path type does not apply.
  • Prefer ActionResult<T> on public APIs so Swagger documents the 200 schema correctly.
  • IActionResult alone often shows a weaker schema unless you add [ProducesResponseType] attributes.
  • Minimal APIs use Results / TypedResults instead of these MVC types, but the same idea applies: declare status and body clearly.
  • Do not return null as a stand-in for 404. Return NotFound() so the status code is correct.
  • ControllerBase helpers (Ok, CreatedAtAction, Conflict) all produce IActionResult implementations.
Flow diagram: Action results
C#
[HttpGet("{id}")] public ActionResult<ProductDto> Get(int id) { var product = _repo.Find(id); if (product is null) return NotFound(); return product; }
08ASP.NET Core & EFFoundation

Model binding sources

Answer

  • Model binding maps HTTP input into action parameters and model properties before the action runs.
  • [FromRoute] binds path segments. [FromQuery] binds the query string. [FromHeader] binds headers.
  • [FromBody] reads the body with input formatters (usually JSON). Only one body parameter is allowed per action.
  • [FromForm] binds form fields and is required for multipart file uploads with IFormFile.
  • Simple types often bind from route or query by convention. Complex types in API controllers often bind from the body.
  • Binding fails populate ModelState. Check ModelState.IsValid or use automatic 400 filters (ApiController attribute).
  • [BindRequired] and [Required] catch missing values; they are not a substitute for authorization checks.
  • Custom model binders implement IModelBinder when conventions cannot map a type (for example a composite search key).
  • Record types and constructors bind if parameter names match. Mismatched names silently leave defaults.
  • Overposting risk: binding onto a rich entity can set fields the client should never control. Prefer dedicated input models.
Flow diagram: Model binding
C#
[HttpPut("{id}")] public IActionResult Update( [FromRoute] int id, [FromQuery] bool notify, [FromBody] UpdateOrderDto dto) { _orders.Update(id, dto, notify); return NoContent(); }
09ASP.NET Core & EFFoundation

Global exception handling

Answer

  • Centralize unhandled exceptions with UseExceptionHandler or IExceptionHandler instead of try/catch in every action.
  • In Development, UseDeveloperExceptionPage shows detailed stacks. Do not enable that in Production.
  • Return ProblemDetails (RFC 7807) for consistent API error bodies: type, title, status, detail, instance.
  • Log the exception once with a correlation id. Duplicate logs from rethrows confuse operators.
  • Do not leak stack traces, SQL text, or connection strings to clients in Production.
  • Map known domain exceptions to 400/404/409. Leave unknown exceptions as 500 after logging.
  • Exception-handling middleware should be early in the pipeline so later middleware failures are caught.
  • MVC exception filters can handle errors inside the MVC action stack; middleware catches failures outside that stack too.
  • For minimal APIs and controllers together, prefer one shared exception handler path.
  • Health check failures and intentional Result-based errors are not the same as unhandled exceptions. Do not conflate them.
Flow diagram: Exception middleware
C#
app.UseExceptionHandler(err => { err.Run(async context => { context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = "application/problem+json"; await context.Response.WriteAsJsonAsync(new ProblemDetails { Title = "Unexpected error", Status = 500, }); }); });
10ASP.NET Core & EFFoundation

HTTP verbs and status codes

Answer

  • GET reads. POST creates or triggers non-idempotent work. PUT replaces a resource. PATCH applies a partial update. DELETE removes.
  • PUT and DELETE are idempotent by design when implemented correctly. POST usually is not.
  • 201 Created should include a Location header (or equivalent) for the new resource when clients need to follow it.
  • 204 No Content means success with no body. 200 OK often returns a representation.
  • 401 means missing or invalid authentication. 403 means authenticated but not allowed.
  • 400 is a client input problem. 404 means the resource (or sometimes the route) was not found. 409 signals conflict.
  • 422 is used by some APIs for semantic validation failures; 400 is also common. Pick a convention and document it.
  • 500 is an unhandled server failure. 503 means unavailable, often temporary; pair with Retry-After when you can.
  • Do not use GET for state changes. Caches and crawlers may replay GET.
  • REST style is a convention. Interviews expect you to justify verb and status choices for your resource model.
Flow diagram: HTTP verbs
C#
[HttpPost] public ActionResult<OrderDto> Create(CreateOrderDto dto) { var order = _orders.Create(dto); return CreatedAtAction(nameof(Get), new { id = order.Id }, order); // 201 } [HttpGet("{id}")] public ActionResult<OrderDto> Get(int id) => _orders.Find(id) is { } order ? order : NotFound();
11ASP.NET Core & EFFoundation

API pagination

Answer

  • Accept page and pageSize, or a cursor, as query parameters. Document defaults and maximums.
  • Apply Skip/Take (or keyset comparisons) on IQueryable so the database returns one page, not the full table.
  • Offset pagination (Skip) gets slow on large offsets. Keyset/cursor pagination scales better for infinite scroll.
  • Return total count only when the UI needs it. COUNT(*) on huge tables can cost as much as the page query.
  • Cap pageSize to protect memory and database time. Reject or clamp absurd values.
  • Stable ordering is required. Without ORDER BY, pages can duplicate or skip rows as data changes.
  • Include pagination metadata (next cursor, hasMore, or totalPages) in a wrapper DTO or Link headers.
  • For APIs, prefer 200 with an empty list over 404 when the page is empty but the collection exists.
  • EF Core: compose filters first, then OrderBy, then Skip/Take. Avoid ToList before paging.
  • Cursor tokens should be opaque and signed or encrypted if they embed keys the client must not forge.
Flow diagram: Pagination
C#
[HttpGet] public async Task<IActionResult> List(int page = 1, int pageSize = 20) { pageSize = Math.Clamp(pageSize, 1, 100); var query = _db.Products.AsNoTracking().OrderBy(p => p.Id); var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); return Ok(new { page, pageSize, items }); }
12ASP.NET Core & EFFoundation

Eager, lazy, and explicit loading

Answer

  • Eager loading uses Include and ThenInclude so related data arrives in the same query (or split queries).
  • Explicit loading calls Entry(entity).Collection(...).Load() or Reference(...).Load() after the root is loaded.
  • Lazy loading proxies load navigations on first access when proxies and lazy loading are enabled.
  • Lazy loading easily causes N+1 queries in loops and serializers that touch navigations.
  • Prefer eager loading or Select projections for APIs. Keep lazy loading off unless you control every access path.
  • AsSplitQuery avoids cartesian product blow-ups when Include joins many collections.
  • Projection to DTOs often outperforms Include graphs because you fetch only needed columns.
  • After AsNoTracking, lazy loading generally does not work because tracking proxies are not created the same way.
  • Explicit loading is useful when the need for a relation depends on a branch after the root load.
  • Watch SQL logs or EF Core logging to verify you got one query, not one per row.
Flow diagram: EF loading
C#
var order = await _db.Orders .Include(o => o.Lines) .AsNoTracking() .FirstAsync(o => o.Id == id);
13ASP.NET Core & EFFoundation

Find vs FirstOrDefault

Answer

  • Find(key) looks up by primary key values and checks the change tracker before hitting the database.
  • If the entity is already tracked, Find returns it without a round trip.
  • FirstOrDefault(predicate) always builds a database query (unless the provider can optimize). Use it for non-key filters.
  • First throws InvalidOperationException when the sequence is empty. FirstOrDefault returns null/default.
  • Single / SingleOrDefault throw when more than one row matches. Use them when uniqueness is required.
  • Find only works with primary key. Composite keys take multiple arguments in key order.
  • Find does not accept arbitrary predicates or Include. For graphs, query with FirstOrDefault and Include, or load then explicit-load.
  • In ASP.NET, prefer FindAsync and FirstOrDefaultAsync so request threads are not blocked.
  • After Find returns null, do not assume the row never existed if a different context already deleted it in another request.
  • For read-only API gets by id, FirstOrDefaultAsync with AsNoTracking is common when you do not need tracker semantics.
Flow diagram: Find vs First
C#
var tracked = await _db.Products.FindAsync(id); var read = await _db.Products .AsNoTracking() .FirstOrDefaultAsync(p => p.Sku == sku);
14ASP.NET Core & EFFoundation

First, Single, and OrDefault

Answer

  • First: returns the first match ordered by the query. Throws if the sequence is empty.
  • FirstOrDefault: first match, or default (null for reference types) if empty.
  • Single: exactly one match. Throws if zero or if more than one.
  • SingleOrDefault: zero or one match. Returns default if zero. Throws if more than one.
  • Pick Single when the filter must be unique (natural key, unique index). Duplicates are data bugs you want to surface.
  • Pick First when order matters and duplicates are allowed (latest by date, any matching row).
  • Prefer FirstOrDefault for optional lookups when you do not want to throw yet on empty results.
  • Without OrderBy, First is nondeterministic among ties. Add OrderBy when "first" means something business-specific.
  • In EF Core these translate to SQL TOP/FETCH with appropriate cardinality expectations; Single may use TOP 2 to detect multiples.
  • Do not catch InvalidOperationException as normal control flow for empty results. Use OrDefault variants instead.
Flow diagram: First vs Single
C#
var any = await db.Products.FirstOrDefaultAsync(p => p.Sku == sku); var unique = await db.Products.SingleAsync(p => p.Id == id); // SingleOrDefaultAsync: 0 → null, 1 → entity, 2+ → exception
15ASP.NET Core & EFIntermediate

ASP.NET Core Middleware Pipeline Architecture

Answer

  • The ASP.NET Core HTTP request pipeline is composed of a sequence of middleware components organized as a bidirectional Russian-doll execution chain. Each component receives an HttpContext and a RequestDelegate representing the next middleware in line.
  • Pipeline Direction: When a request arrives, middleware executes in the exact order declared in Program.cs. Once the terminal endpoint produces a response, execution unwinds in reverse order back up the chain, allowing components to inspect status codes, log execution duration, or modify response headers.
  • Short-Circuiting: A middleware can decide not to call next(context). By writing to the response body and returning directly, it short-circuits the pipeline, preventing subsequent middleware, authentication, or controller actions from executing.
  • Terminal Middleware vs Filters: Middleware sees every raw HTTP request passing through the server socket. MVC/API Filters run much later, only after routing has matched the request to a specific controller action, giving filters access to action arguments and model state.
Flow diagram: Bidirectional middleware pipeline execution
C# (Custom Middleware)
public class RequestTimingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestTimingMiddleware> _logger; public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var stopwatch = Stopwatch.StartNew(); // Hook into response headers before they are flushed over the socket context.Response.OnStarting(() => { context.Response.Headers["X-Response-Time-Ms"] = stopwatch.ElapsedMilliseconds.ToString(); return Task.CompletedTask; }); try { await _next(context); // Call the next middleware in the pipeline } finally { stopwatch.Stop(); _logger.LogInformation("HTTP {Method} {Path} responded {StatusCode} in {Elapsed}ms", context.Request.Method, context.Request.Path, context.Response.StatusCode, stopwatch.ElapsedMilliseconds); } } }

Ordering and Critical Pipeline Traps

  • Standard Execution Order: (1) Exception Handling / Diagnostics, (2) HSTS / HTTPS Redirection, (3) Static Files, (4) Routing, (5) CORS, (6) Authentication, (7) Authorization, (8) Custom Business Middleware, (9) Endpoint Execution.
  • Headers Sent Trap: Once a middleware or endpoint starts streaming the response body, HTTP response headers are committed and sent over the wire. If downstream code throws an exception after headers are committed, an upstream UseExceptionHandler cannot change the status code to 500; the TCP connection simply resets.
  • Branching Pipelines: app.Map() branches based on request URL prefix. app.MapWhen() branches based on a boolean predicate on HttpContext. app.UseWhen() branches conditionally but rejoins the main pipeline if the branch does not short-circuit.
16ASP.NET Core & EFIntermediate

Authentication basics

Answer

  • Authentication proves who the caller is. Authorization decides what that identity may do.
  • ASP.NET Core authentication middleware builds a ClaimsPrincipal on HttpContext.User from a configured scheme.
  • Cookie authentication fits browser apps on the same site. Pair it with antiforgery tokens on state-changing POSTs.
  • JWT bearer authentication fits SPAs, mobile apps, and service clients that send Authorization: Bearer tokens.
  • OpenID Connect / OAuth2 hand off login to an identity provider (Entra ID, IdentityServer, Auth0) and return tokens or cookies.
  • Schemes have names. Challenge and Forbid select which scheme runs. DefaultAuthenticateScheme runs on each request.
  • Store secrets (client secrets, signing keys) in configuration providers, user secrets, or Key Vault. Never commit them.
  • Authentication does not replace HTTPS. Tokens and cookies on plain HTTP are easy to steal.
  • Sign-in creates the auth ticket. Sign-out clears cookies or instructs clients to drop tokens.
  • Fail closed: unauthenticated callers should not reach protected resources because a scheme was misconfigured to no-op.
Flow diagram: AuthN vs AuthZ
C#
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "https://issuer.example", ValidateAudience = true, ValidAudience = "api", ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(signingKeyBytes), }; });
17ASP.NET Core & EFIntermediate

Authorization

Answer

  • Authorization runs after authentication. It uses roles, claims, and policies against HttpContext.User.
  • [Authorize] requires an authenticated user by default. [AllowAnonymous] opts an endpoint out.
  • Policies register named rules (RequireClaim, RequireRole, or custom IAuthorizationRequirement + handler).
  • Use RequireAuthorization("PolicyName") on minimal APIs and MapGroup the same way controllers use [Authorize(Policy = ...)].
  • Resource-based authorization loads the resource then calls AuthorizeAsync with that resource so handlers can check ownership.
  • Roles are often just claims. Prefer policies over scattering magic role strings if rules grow.
  • Fail closed: if metadata is missing or misapplied, treat access as denied in production reviews.
  • 403 Forbidden means authenticated but not allowed. 401 Unauthorized means missing or invalid authentication.
  • Do not implement authZ only in the UI. Every API endpoint must enforce it on the server.
  • IAuthorizationHandler can be registered in DI. Keep handlers focused; heavy I/O belongs behind a clear cache if called often.
Flow diagram: Authenticate, then authorize
C#
builder.Services.AddAuthorization(options => { options.AddPolicy("StaffOnly", p => p.RequireRole("Staff").RequireClaim("dept", "ops")); }); var app = builder.Build(); app.MapGet("/reports", () => "ok") .RequireAuthorization("StaffOnly");
18ASP.NET Core & EFIntermediate

Options pattern

Answer

  • Bind a configuration section to a POCO instead of scattering configuration["key"] calls through the app.
  • services.Configure<T>(configuration.GetSection("Name")) registers the binding. Inject IOptions<T>, IOptionsSnapshot<T>, or IOptionsMonitor<T>.
  • IOptions<T> is a singleton snapshot resolved once. It does not see reloadable config updates.
  • IOptionsSnapshot<T> is scoped and re-reads when configuration reloads, which fits per-request settings.
  • IOptionsMonitor<T> is singleton-friendly and raises OnChange when the underlying config changes.
  • DataAnnotations plus ValidateDataAnnotations, or IValidateOptions<T>, fail startup when values are invalid.
  • ValidateOnStart() (or host validation) catches bad config before traffic arrives.
  • Named options (IOptionsMonitor.Get("name")) support multiple sections of the same type (for example several Smtp accounts).
  • Keep option classes immutable in spirit: public get; init; or get; set; with no behavior. Put behavior in services that consume options.
  • Do not inject IConfiguration everywhere once Options exist. Typed options document the settings contract.
Flow diagram: Options pattern
C#
public class SmtpOptions { public const string Section = "Smtp"; public string Host { get; set; } = ""; public int Port { get; set; } = 587; } builder.Services.Configure<SmtpOptions>( builder.Configuration.GetSection(SmtpOptions.Section)); public class Mailer(IOptions<SmtpOptions> options) { private readonly SmtpOptions _opt = options.Value; }
19ASP.NET Core & EFIntermediate

DTOs

Answer

  • A DTO (data transfer object) shapes what crosses an API or process boundary, separate from persistence entities.
  • DTOs hide internal fields, reduce overposting, and avoid circular navigation graphs when serializing JSON.
  • Map entities to DTOs in the application/service layer or with a mapper. Controllers stay thin.
  • Do not expose EF entities as API contracts by default. Lazy loads and proxy types serialize poorly and leak schema.
  • Input DTOs (commands) and output DTOs (queries) can differ. CreateOrderRequest need not match OrderDetailsResponse.
  • Version DTOs carefully. Additive optional fields are safer than renaming or removing properties.
  • Record types work well as immutable DTOs in modern C#.
  • Validation attributes or FluentValidation belong on input DTOs, not on EF entities shared with the database model.
  • Projection (Select into DTO) in EF Core pushes shaping into SQL and skips tracking.
  • Keep mapping boring and explicit when teams struggle with magic mappers hiding missing fields.
Flow diagram: DTO boundary
C#
public class ProductDto { public int Id { get; init; } public string Name { get; init; } = ""; public decimal SellPrice { get; init; } } // CostPrice stays on the entity, never on the DTO.
20ASP.NET Core & EFIntermediate

CORS (Cross-Origin Resource Sharing)

Answer

  • The Same-Origin Policy (SOP) is a foundational browser security boundary that blocks client-side scripts running on one origin (protocol, domain, port) from reading responses fetched from a different origin. Without SOP, a malicious script running on evil.com could use an active user session in the browser to read private financial data from api.bank.com.
  • CORS is a W3C standard that allows servers to loosen this browser restriction for trusted frontend origins. The server returns dedicated HTTP headers informing the browser that the calling origin is authorized to read the response payload.
  • CORS is enforced exclusively by web browsers. Backend services, curl, Postman, and native mobile apps do not enforce the Same-Origin Policy and do not process CORS headers. CORS protects the user in their browser; it does not protect an API server from direct unauthorized requests.
  • In client-side JavaScript, a CORS failure causes fetch() to reject with a generic TypeError: Failed to fetch without providing the underlying HTTP status code or response body, because the browser deliberately hides the unpermitted payload from script inspection.
Sequence diagram: Preflight OPTIONS exchange and actual request lifecycle
C# (ASP.NET Core)
var builder = WebApplication.CreateBuilder(args); var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? new[] { "https://app.example.com" }; builder.Services.AddCors(options => { options.AddPolicy("ProductionCorsPolicy", policy => { policy.WithOrigins(allowedOrigins) .AllowAnyMethod() .WithHeaders("Authorization", "Content-Type", "X-Request-ID") .AllowCredentials() .SetPreflightMaxAge(TimeSpan.FromHours(24)) .WithExposedHeaders("X-Pagination-TotalPages", "Content-Disposition"); }); }); var app = builder.Build(); // Pipeline ordering is critical: app.UseRouting(); // UseCors MUST run after UseRouting and BEFORE UseAuthentication/UseAuthorization app.UseCors("ProductionCorsPolicy"); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();

How CORS Works in HTTP Headers

  • Origin (Request Header): The browser automatically sets this header on cross-origin requests to the protocol, domain, and port of the calling page (for example, Origin: https://app.example.com). JavaScript cannot modify or spoof this header.
  • Access-Control-Allow-Origin (Response Header): The server specifies which origins can read the response. It can return an exact origin (https://app.example.com) or a wildcard (*). When cookies or authorization headers are included, the wildcard is forbidden by the CORS specification; the server must return the exact matching origin.
  • Access-Control-Allow-Methods (Response Header): Informs the browser which HTTP verbs are permitted for cross-origin requests (GET, POST, PUT, DELETE, OPTIONS).
  • Access-Control-Allow-Headers (Response Header): Lists non-standard request headers that the client is permitted to send (such as Authorization, Content-Type, or X-Request-ID).
  • Access-Control-Allow-Credentials (Response Header): When set to true, allows the browser to send cookies, HTTP authentication, and TLS client certificates with the request, and allows JavaScript to read the response. The frontend must also set credentials: "include" on the fetch call.
  • Access-Control-Max-Age (Response Header): Specifies how many seconds the browser may cache the preflight OPTIONS response. Caching prevents every subsequent mutating request from paying the latency penalty of an extra network round-trip.
  • Access-Control-Expose-Headers (Response Header): By default, browsers only let JavaScript inspect basic response headers (Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma). Any custom header (like X-Total-Count or Content-Disposition) must be explicitly listed here to be read in code.

Simple Requests vs Preflighted Requests

  • Simple Requests: The browser sends the real request immediately without asking permission first if: the method is GET, HEAD, or POST; request headers are limited to standard safe headers (Accept, Accept-Language, Content-Language, Content-Type); and the Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain. The browser sends the Origin header, and checks if the server returned a valid Access-Control-Allow-Origin before exposing the response.
  • Preflighted Requests: If a request uses a JSON body (application/json), a custom header (like Authorization), or an HTTP verb like PUT, PATCH, or DELETE, the browser first transmits an HTTP OPTIONS preflight request. This preflight checks whether the server approves the origin, method, and headers. The browser only sends the actual business request if the preflight returns HTTP 200/204 with matching Access-Control-Allow-* headers.

Trade-offs and Common Production Traps

  • Pros: Allows Single Page Applications (SPAs) hosted on CDNs or separate domains to safely communicate with centralized backend APIs and microservices without insecure workarounds like JSONP or reverse-proxy routing hacks.
  • Cons: Preflight requests introduce an additional network round-trip on every uncached mutating call, increasing latency on high-ping mobile networks unless Access-Control-Max-Age is properly configured.
  • ASP.NET Core Middleware Order Trap: app.UseCors() must be placed between app.UseRouting() and app.UseAuthentication() / app.UseAuthorization(). If UseCors is placed after UseAuthorization, unauthenticated preflight OPTIONS requests fail with HTTP 401 before CORS headers can be written, resulting in a confusing CORS error in the browser console.
  • Security Fallacy Trap: Exposing an API to all origins with AllowAnyOrigin() does not protect against data scraping or server attacks from non-browser clients. True security requires bearer tokens, rate limiting, and server-side authorization.
21ASP.NET Core & EFIntermediate

Middleware vs Filters in ASP.NET Core

Answer

  • Both middleware and filters execute code before and after HTTP request handling, but they operate at different abstraction levels in the ASP.NET Core architecture.
  • Scope of Execution: Middleware runs on every HTTP request hitting the server (including static files and unrecognized endpoints). Filters run only after routing has matched a request to an MVC or API controller action.
  • Contextual Awareness: Middleware operates on raw HttpContext with no awareness of controllers, actions, or parameter types. Filters have access to ActionExecutedContext, ActionArguments, and ModelState, allowing direct validation inspection and result modification.
  • Short-circuiting Behavior: Middleware short-circuits by not calling next() and writing directly to the response body. Action filters short-circuit by assigning an IActionResult to context.Result, letting MVC format the response according to content negotiation rules.
Flow diagram: Request flow through middleware and MVC filter stages
C# (Action Filter for Model Validation)
public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { // Short-circuit action and return structured 422 ProblemDetails context.Result = new UnprocessableEntityObjectResult(new ValidationProblemDetails(context.ModelState)); } } }

Filter Types and Pipeline Placement

  • Authorization Filters: Run first to determine if the caller has permission for the action.
  • Resource Filters: Run before model binding; useful for caching whole action outputs.
  • Action Filters: Run immediately before and after the action method executes; inspect action arguments and model state.
  • Exception Filters: Catch unhandled exceptions thrown by controller actions. Middleware exception handlers (UseExceptionHandler) are preferred for application-wide logging because exception filters miss errors occurring in middleware, routing, and serialization.
  • Result Filters: Run before and after the action result executes; inspect or modify headers on the produced result.
22ASP.NET Core & EFAdvanced

ADO.NET Connection Pooling & Socket Mechanics

Answer

  • ADO.NET is the underlying data access foundation in .NET that powers Entity Framework Core, Dapper, and raw SQL commands. A SqlConnection represents an open TCP socket connection to a database engine (such as SQL Server).
  • Connection Pooling: Opening physical TCP sockets, performing TLS handshakes, and authenticating user credentials costs 20-100 ms of latency per connection. ADO.NET automatically enables connection pooling by default to amortize this cost.
  • How the Pool Works: When connection.Close() or connection.Dispose() is called, the underlying physical TCP socket is not terminated; it is cleaned up (sp_reset_connection is called to clear temporary tables, active transactions, and user context) and returned to an internal pool queue. When connection.Open() is subsequently called with an identical connection string, ADO.NET pulls an already-open physical connection from the pool in sub-millisecond time.
  • Pool Identity: Connection pools are partitioned by the exact, case-sensitive connection string. Extra spaces, reordered keys, or dynamic connection strings spawn separate pools, exhausting database server session limits.
Flow diagram: ADO.NET connection pool lease and return lifecycle
C# (Robust ADO.NET Command with Cancellation)
public async Task<List<ProductDto>> GetActiveProductsAsync(string connectionString, CancellationToken ct) { var results = new List<ProductDto>(); // await using guarantees connection returns to pool even on cancellation/exception await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(ct); const string sql = """ SELECT ProductId, Name, Price FROM dbo.Products WHERE IsActive = @IsActive ORDER BY Name; """; await using var cmd = new SqlCommand(sql, conn); cmd.Parameters.Add("@IsActive", SqlDbType.Bit).Value = true; // CommandBehavior.CloseConnection ensures stream disposal await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection, ct); while (await reader.ReadAsync(ct)) { results.Add(new ProductDto { ProductId = reader.GetInt32(0), Name = reader.GetString(1), Price = reader.GetDecimal(2) }); } return results; }

Pool Starvation and Leak Prevention

  • Connection Leaks: If an application creates a SqlConnection and fails to dispose it (for example, failing to wrap it in a using or await using block when an exception occurs), the physical socket remains checked out of the pool indefinitely. Once active connections reach Max Pool Size (default 100), future Open() calls block until they throw an InvalidOperationException: "Timeout expired. The timeout elapsed prior to obtaining a connection from the pool."
  • Asynchronous Connections: Always call OpenAsync(), ExecuteReaderAsync(), and ExecuteNonQueryAsync() in web applications. Synchronous calls block ThreadPool worker threads during network I/O, triggering thread pool starvation under high concurrent load.
23ASP.NET Core & EFAdvanced

Advanced: EF identity map traps

Answer

  • A DbContext tracks at most one CLR instance per primary key. A second Attach or Add with the same key throws or conflicts.
  • Tracking queries resolve by key: if the key is already tracked, you get that instance even if the database row changed.
  • Stale tracked data stays until Reload, Entry.Reload, or a new context. This bites background workers that reuse one context.
  • AsNoTracking can return a different object instance for a key you already track. Mixing tracked and untracked graphs causes subtle bugs.
  • Include on a tracked query still identity-resolves related entities already in the tracker.
  • Keep contexts short-lived (per request / unit of work). Long-lived contexts grow memory and serve stale identity-mapped rows.
  • When mapping DTOs back to entities, load by key first or use a clear attach strategy. Do not new up a second instance with the same key while tracked.
  • In tests, one context for arrange and assert can hide bugs that appear with a fresh context per operation.
  • NoTracking with IdentityResolution still dedupes instances within that query result, but does not merge into the change tracker the same way.
Flow diagram: One tracked instance per key
C#
await using var db = new AppDbContext(); var a = await db.Orders.FirstAsync(o => o.Id == id); var b = await db.Orders.FirstAsync(o => o.Id == id); ReferenceEquals(a, b); // true: both tracked queries var c = await db.Orders.AsNoTracking().FirstAsync(o => o.Id == id); ReferenceEquals(a, c); // false: no-tracking returns its own instance
24ASP.NET Core & EFAdvanced

Optimistic Concurrency and RowVersion in EF Core

Answer

  • Pessimistic vs Optimistic Locking: Pessimistic locking acquires exclusive database locks (SELECT ... WITH (XLOCK)) for the duration of a transaction, preventing other readers and writers from proceeding. Optimistic locking avoids database-level locks; it checks during UPDATE whether the underlying record changed since it was loaded into memory.
  • RowVersion (byte[] / TIMESTAMP): In SQL Server, a rowversion column automatically increments an 8-byte binary counter on every INSERT or UPDATE to the row. EF Core maps this column as a concurrency token via [Timestamp] or .IsRowVersion().
  • Update Mechanics: When saving changes, EF Core appends the rowversion to the WHERE clause: UPDATE Orders SET Status = @p0 WHERE Id = @p1 AND RowVersion = @p2. If a concurrent transaction updated the record first, the WHERE clause matches 0 rows, and EF Core throws a DbUpdateConcurrencyException.
  • Conflict Resolution Strategies: When a concurrency exception occurs, application code can inspect Entry.GetDatabaseValues(), Entry.OriginalValues, and Entry.CurrentValues to execute Client-Wins (overwrite), Database-Wins (abort), or Field-Level Merging.
Sequence diagram: Optimistic concurrency detection using RowVersion token
C# (Handling DbUpdateConcurrencyException)
public async Task<bool> UpdateAccountBalanceAsync(Guid accountId, decimal delta, AppDbContext db) { var account = await db.Accounts.FindAsync(accountId); if (account is null) return false; account.Balance += delta; try { await db.SaveChangesAsync(); return true; } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var databaseValues = await entry.GetDatabaseValuesAsync(); if (databaseValues is null) throw new InvalidOperationException("The account was deleted by another user."); // Reload fresh database values to reconcile entry.OriginalValues.SetValues(databaseValues); return false; // Signal caller to retry with updated balance } }
25ASP.NET Core & EFAdvanced

JWT Authentication Mechanics and Token Lifecycle

Answer

  • JSON Web Token (RFC 7519) is a compact, URL-safe standard for transmitting cryptographically verifiable claims between parties. A JWT consists of three Base64Url-encoded segments joined by dots: Header, Payload, and Signature (header.payload.signature).
  • Header: Declares the signing algorithm (such as HMAC-SHA256 (HS256) or RSA-SHA256 (RS256)) and token type (JWT).
  • Payload: Contains claims (statements about the user and metadata). Standard registered claims include: sub (subject/user ID), iss (issuer), aud (audience), exp (expiration timestamp), nbf (not before), and iat (issued at). Custom claims include user roles, tenant IDs, and feature permissions. The payload is Base64Url encoded, NOT encrypted; never store passwords or sensitive secrets inside a JWT payload.
  • Signature: Computed by hashing the encoded header and payload with a secret key: HMACSHA256(base64Url(header) + "." + base64Url(payload), secret). The receiving server verifies this cryptographic signature to prove that the claims have not been tampered with.
Sequence diagram: Asymmetric JWT issuance and stateless verification
C# (ASP.NET Core JWT Validation)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "https://auth.example.com", ValidateAudience = true, ValidAudience = "api://ecommerce", ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), // Prevent clock drift rejects ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!)) }; });

Symmetric (HS256) vs Asymmetric (RS256) Signing

  • Symmetric (HS256): A single shared secret key is used both by the authorization server to sign the token and by all microservices to verify it. If any microservice is compromised, an attacker can forge arbitrary tokens for the entire system.
  • Asymmetric (RS256 / ES256): The Identity Provider (IdP) holds a private key to sign tokens, while resource servers only hold the public key (often fetched via a standard JWKS endpoint: /.well-known/jwks.json). Resource servers verify token validity without possessing the capability to forge tokens.

Revocation and Token Expiration Strategy

  • Statelessness vs Revocation: Because JWTs are verified statelessly by verifying signatures against the public key without querying a database on every request, an active token cannot be revoked immediately before its exp expiration time without introducing state.
  • Access Token + Refresh Token Pattern: Issue short-lived access tokens (10-15 minutes) kept in memory. Issue long-lived refresh tokens (7-30 days) stored securely in HttpOnly, SameSite cookies. Refresh tokens are tracked in a database table or Redis cache. When user permissions change or a logout occurs, the server revokes the refresh token, preventing the client from refreshing its access token.
Sources and further reading