The 60 ASP.NET questions interviewers actually ask, with direct answers, runnable C# code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
ASP.NET is Microsoft's open-source framework for building web applications, services, and APIs on the .NET runtime, written mostly in C#. Every request flows through a pipeline of middleware components before your handler runs, and dependency injection is built into the framework from the start rather than bolted on. The modern version, ASP.NET Core, is a ground-up rewrite that runs cross-platform on Windows, Linux, and macOS and unifies MVC, Web API, and Razor Pages under one hosting model. In interviews, ASP.NET questions probe the request pipeline, middleware ordering, DI lifetimes, and the trade-offs between MVC, Razor Pages, Minimal APIs, and Web API, not memorized attribute names. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If you're still building fundamentals, the official ASP.NET Core documentation from Microsoft is the canonical reference; increasingly the first round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Learn ASP.NET Core MVC (.NET 6) - Full Course
Video: Learn ASP.NET Core MVC (.NET 6) - Full Course (freeCodeCamp.org, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your ASP.NET certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
ASP.NET is Microsoft's open-source framework for building web applications, services, and APIs on the .NET runtime, written mostly in C#. It handles routing, request processing, security, and rendering so you focus on your app's logic.
It's used for server-rendered sites (MVC, Razor Pages), REST and gRPC APIs (Web API, Minimal APIs), and real-time apps (SignalR). The modern version is ASP.NET Core, which runs cross-platform.
Key point: A one-line definition plus two concrete use cases beats a memorized feature list. Interviewers open with this to hear how you organize an answer.
Watch a deeper explanation
Video: Learn ASP.NET Core MVC (.NET 6) - Full Course (freeCodeCamp.org, YouTube)
ASP.NET (the classic version) runs only on Windows on the .NET Framework. ASP.NET Core is a rewrite that runs cross-platform on Windows, Linux, and macOS, is open-source, and is faster.
Core also unifies MVC, Web API, and Razor Pages under one model, has built-in dependency injection, and uses a modern middleware pipeline. All new work targets Core; classic ASP.NET is in maintenance for legacy apps.
| ASP.NET (Framework) | ASP.NET Core | |
|---|---|---|
| Platform | Windows only | Windows, Linux, macOS |
| Open source | Partly | Fully |
| Performance | Lower | Higher |
| Status | Maintenance | Actively developed |
Key point: Interviewers ask this to check whether you know Core is the default now. Say 'all new development targets Core' out loud.
Middleware is a component that sits in the request pipeline. Each piece can inspect or change the request, then either pass it to the next component or short-circuit and return a response early.
You register middleware in order with app.Use..., and order matters: exception handling first, then routing, authentication, authorization, and finally your endpoints. Middleware is how logging, CORS, and auth plug in.
app.Use(async (context, next) =>
{
// runs on the way in
Console.WriteLine($"Request: {context.Request.Path}");
await next();
// runs on the way out
Console.WriteLine($"Response: {context.Response.StatusCode}");
});Key point: Draw the pipeline as a chain if you can. Showing that order matters (auth before endpoints) is what this question screens for.
Watch a deeper explanation
Video: ASP.NET Core Crash Course - C# App in One Hour (freeCodeCamp.org, YouTube)
The request pipeline is the ordered chain of middleware every HTTP request passes through before a response goes back. A request enters, flows through each middleware in registration order, reaches an endpoint, then flows back out in reverse.
You build it in Program.cs. Common stages: exception handling, HTTPS redirection, static files, routing, authentication, authorization, then endpoint execution.
var app = builder.Build();
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();How a request flows through the ASP.NET Core pipeline
Order matters: put UseRouting before UseAuthorization, and exception handling first so it wraps everything after it.
ASP.NET uses C#, a statically typed, object-oriented language that compiles to run on the .NET runtime. You can also use F# or VB.NET, but C# is the default and by far the most common.
C# gives you strong typing, async/await, LINQ for querying collections, generics, and garbage collection. Knowing the language matters because ASP.NET questions often blend framework and C# concepts.
MVC splits a web app into three parts: the Model (data and business logic), the View (the HTML the user sees), and the Controller (handles requests, calls the model, picks a view). It keeps concerns separated so code stays testable and organized.
In ASP.NET Core, a controller is a class with action methods; routing maps a URL to an action; the action returns a view or data. Web API uses the same controller model but returns data instead of HTML.
public class ProductsController : Controller
{
public IActionResult Index()
{
var products = _repo.GetAll(); // model
return View(products); // view
}
}Watch a deeper explanation
Video: Introduction to ASP.NET Core MVC in C# plus LOTS of Tips (IAmTimCorey, YouTube)
A controller is a class that groups related request handlers. An action method is a public method on that controller that responds to a specific route, for example GET /products/5 maps to a Get(int id) action.
Actions return results: a view, JSON, a file, or a status code. Routing decides which action runs based on the URL, HTTP verb, and route templates.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var product = _repo.Find(id);
return product is null ? NotFound() : Ok(product);
}
}Routing maps an incoming URL to the code that handles it. ASP.NET Core matches the request path and HTTP verb against route templates and picks an endpoint (a controller action, Razor Page, or minimal-API handler).
Attribute routing (putting [Route] and [HttpGet] on actions) is the norm for APIs. Conventional routing (a central pattern like {controller}/{action}/{id?}) is common in MVC views.
// attribute routing
[HttpGet("api/products/{id:int}")]
public IActionResult Get(int id) => Ok(_repo.Find(id));
// conventional routing
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");Web API is the part of ASP.NET Core for building HTTP services that return data (usually JSON) instead of HTML. It's how you build REST backends for mobile apps, single-page apps, and other services.
Controllers inherit from ControllerBase, carry the [ApiController] attribute, and return data or IActionResult. The framework handles content negotiation, so the same action can return JSON or XML based on the request.
GET retrieves data and should not change server state; its parameters come from the route or query string. POST sends data to create or change something, and its payload comes from the request body.
In ASP.NET Core you mark them with [HttpGet] and [HttpPost]. GET is safe and idempotent by convention; POST is neither, which is why forms and API creates use POST.
| GET | POST | |
|---|---|---|
| Purpose | Read data | Create or submit data |
| Data location | URL and query string | Request body |
| Idempotent | Yes | No |
| Cacheable | Often | Rarely |
Razor is the templating syntax that mixes C# with HTML in .cshtml files using the @ symbol. It renders dynamic HTML on the server, so you can loop over data or show values inline.
Razor Pages is a page-focused model where each page has its own .cshtml view and a PageModel code file. It's simpler than MVC for page-centric sites because the handler lives right next to the markup.
<ul>
@foreach (var product in Model.Products)
{
<li>@product.Name: @product.Price.ToString("C")</li>
}
</ul>Dependency injection means a class receives the objects it needs (its dependencies) from outside instead of creating them itself. That makes code loosely coupled and easy to test with fakes.
ASP.NET Core has DI built in. You register services in the container, then declare them as constructor parameters and the framework supplies them. No third-party container is needed for most apps.
// register
builder.Services.AddScoped<IProductRepo, ProductRepo>();
// consume via constructor
public class ProductsController : ControllerBase
{
private readonly IProductRepo _repo;
public ProductsController(IProductRepo repo) => _repo = repo;
}Key point: DI is one of ASP.NET Core's signature features. Being able to register and inject a service from memory is the expected bar.
Watch a deeper explanation
Video: ASP NET Core Tutorial (kudvenkat, YouTube)
Program.cs is the entry point. It creates a builder, registers services in the DI container, builds the app, wires up the middleware pipeline, and calls Run() to start the server.
Since .NET 6 it uses minimal hosting: one file, top-level statements, no separate Startup class. The two halves to know are 'add services' (before Build) and 'configure the pipeline' (after Build).
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();Model binding maps incoming request data onto your action-method parameters. It reads values from the route, query string, form, and request body, then fills in parameters and model objects automatically.
You can guide it with attributes like [FromBody], [FromQuery], and [FromRoute] when the source isn't obvious. With [ApiController], binding sources are inferred for you.
[HttpPost]
public IActionResult Create([FromBody] Product product,
[FromQuery] bool notify)
{
_repo.Add(product);
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}IActionResult is the return type that lets one action return different HTTP responses depending on logic: Ok(data) for 200, NotFound() for 404, BadRequest() for 400, and so on.
It decouples the action from a single result shape. If you always return the same data type you can return that type directly, but IActionResult is right when the status code varies.
public IActionResult Get(int id)
{
var product = _repo.Find(id);
if (product is null) return NotFound();
return Ok(product);
}Entity Framework Core (EF Core) is Microsoft's object-relational mapper. It maps C# classes to database tables so you query and save data with LINQ and objects instead of writing raw SQL.
You define a DbContext with DbSet properties for each table, then query with LINQ. EF Core translates that to SQL, tracks changes, and saves them when you call SaveChanges.
public class ShopContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
}
// query with LINQ
var cheap = await _context.Products
.Where(p => p.Price < 20)
.ToListAsync();appsettings.json holds configuration: connection strings, logging levels, feature flags, and app settings, as JSON. ASP.NET Core reads it at startup and merges it with environment variables and other sources.
You read values through the IConfiguration service or, better, bind a section to a typed options class. Environment-specific files like appsettings.Production.json override the base file.
// appsettings.json
// { "ConnectionStrings": { "Default": "Server=...;Database=Shop" } }
var cs = builder.Configuration.GetConnectionString("Default");
builder.Services.AddDbContext<ShopContext>(o => o.UseSqlServer(cs));A ViewModel is a class shaped for a specific view or response, carrying exactly the data that screen needs. It keeps your database entities separate from what you send to the UI or client.
Using ViewModels (or DTOs for APIs) avoids over-posting bugs, hides internal fields, and lets the view and the database schema change independently.
Tag Helpers let you write HTML-like tags that the server processes into real markup, so forms, links, and inputs stay readable while binding to your model. asp-for, asp-action, and asp-controller are common ones.
They replace the older HTML Helper methods with cleaner syntax that looks like plain HTML, which designers and editors find easier to read.
<form asp-controller="Account" asp-action="Login" method="post">
<input asp-for="Email" />
<span asp-validation-for="Email"></span>
<button type="submit">Log in</button>
</form>200 OK for a successful read, 201 Created after creating a resource, 204 No Content for a successful delete or update with nothing to return, 400 Bad Request for invalid input, 401/403 for auth failures, 404 Not Found for a missing resource, and 500 for server errors.
Returning the right code matters because clients branch on it. Helper methods like Ok(), Created(), NotFound(), and BadRequest() map directly to these.
CORS (Cross-Origin Resource Sharing) is a browser security rule that blocks a page on one origin from calling an API on a different origin unless the API opts in. You hit it when a front-end on one domain calls your API on another.
In ASP.NET Core you register a CORS policy and apply it with middleware, naming the allowed origins, methods, and headers.
builder.Services.AddCors(o => o.AddPolicy("web", p =>
p.WithOrigins("https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod()));
app.UseCors("web");Kestrel is the cross-platform web server built into ASP.NET Core. It listens for HTTP requests and hands them to your pipeline. It's fast and runs the same on Windows, Linux, and macOS.
In production Kestrel often sits behind a reverse proxy like IIS, Nginx, or a cloud load balancer that handles TLS termination and static files, though Kestrel can also face the internet directly.
async marks a method that can pause; await pauses at an I/O call (a database query, an HTTP request) and releases the thread back to the pool until the result is ready. That thread serves other requests meanwhile.
The payoff is throughput: one server handles far more concurrent requests because threads aren't blocked waiting on I/O. Use async all the way down and prefer the ...Async methods EF Core and HttpClient provide.
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _repo.FindAsync(id); // thread freed while waiting
return product is null ? NotFound() : Ok(product);
}For candidates with working experience: pipeline mechanics, DI judgment, and the questions that separate users from understanders.
Transient creates a new instance every time the service is requested. Scoped creates one instance per HTTP request, shared within that request. Singleton creates one instance for the entire app lifetime, shared across all requests.
Pick Scoped for things like a DbContext or a per-request unit of work, Singleton for stateless shared services (config, caches), and Transient for lightweight, stateless helpers.
| Lifetime | Instances | Use for |
|---|---|---|
| Transient | New every injection | Lightweight stateless helpers |
| Scoped | One per request | DbContext, per-request state |
| Singleton | One for the app | Config, caches, stateless shared services |
Key point: The classic trap is injecting a Scoped service (like DbContext) into a Singleton. The that captive-dependency bug and you sound senior.
A captive dependency happens when a longer-lived service holds a reference to a shorter-lived one, for example a Singleton that injects a Scoped DbContext. The Scoped service gets captured and effectively becomes a Singleton, living far longer than intended.
That breaks correctness (a DbContext isn't thread-safe and shouldn't outlive a request) and causes subtle data and concurrency bugs. The fix is to resolve the short-lived service per operation, often via IServiceScopeFactory.
// bug: Scoped DbContext captured by a Singleton
// fix: create a scope per unit of work
public class Worker
{
private readonly IServiceScopeFactory _scopes;
public Worker(IServiceScopeFactory scopes) => _scopes = scopes;
public async Task RunAsync()
{
using var scope = _scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ShopContext>();
await db.SaveChangesAsync();
}
}Middleware runs top to bottom on the way in and bottom to top on the way out, so a component only sees requests handled after it and responses handled before it. Put things in the wrong order and features silently break.
The classic mistake is calling UseAuthorization before UseAuthentication, or putting UseRouting after the endpoints. Authentication must run before authorization, and routing must run before the endpoints it feeds.
Correct middleware order for a typical API
Authentication before authorization; routing before endpoints. Get this order wrong and auth silently does nothing.
You write a class with an InvokeAsync(HttpContext context) method that does its work, then calls the next delegate to continue the pipeline. Register it with app.UseMiddleware<T>(). For quick cases, an inline app.Use lambda works too.
Custom middleware is the right place for cross-cutting concerns that touch every request: correlation IDs, request logging, custom headers, timing.
public class TimingMiddleware
{
private readonly RequestDelegate _next;
public TimingMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
await _next(context);
Console.WriteLine($"{context.Request.Path} took {sw.ElapsedMilliseconds}ms");
}
}
// app.UseMiddleware<TimingMiddleware>();Filters run inside the MVC pipeline, around action execution, and have access to MVC context like the action arguments and the result. Types include authorization, resource, action, exception, and result filters. Middleware runs earlier, around the whole request, and only knows about HttpContext.
Use a filter when you need MVC-specific context (validating a model, wrapping a result, catching exceptions per action). Use middleware for things that apply to every request regardless of MVC.
| Middleware | Action filter | |
|---|---|---|
| Scope | Every request | MVC actions only |
| Context | HttpContext | Action args, model state, result |
| Runs | Around the whole pipeline | Around action execution |
| Good for | Logging, CORS, auth handshake | Model validation, result shaping |
MVC fits large server-rendered apps with many routes and shared views. Razor Pages fits page-focused sites where each page owns its logic. Web API (controllers) fits data services with cross-cutting concerns and many endpoints. Minimal APIs fit small, fast services and microservices with little ceremony.
They share the same runtime, so the choice is about structure and team preference, not capability. A common answer: Razor Pages for content sites, Web API for larger backends, Minimal APIs for lean microservices.
A Minimal API defines endpoints as lambdas directly in Program.cs with app.MapGet, app.MapPost, and so on, no controller class needed. It cuts boilerplate and starts fast, which suits small services.
Controllers give you more structure: attribute conventions, filters, model binding features, and easier organization for large surfaces. Minimal APIs have closed much of that gap but still favor smaller apps.
app.MapGet("/products/{id}", async (int id, IProductRepo repo) =>
{
var product = await repo.FindAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
});You annotate model properties with data-annotation attributes ([Required], [Range], [EmailAddress], [StringLength]) and the framework validates incoming data during model binding. ModelState.IsValid tells you the result.
With [ApiController], an invalid model returns a 400 with error details automatically, so you don't check ModelState by hand. For complex rules, implement IValidatableObject or use a library like FluentValidation.
public class CreateProduct
{
[Required, StringLength(100)]
public string Name { get; set; } = "";
[Range(0.01, 100000)]
public decimal Price { get; set; }
}
// with [ApiController], invalid input auto-returns 400By default EF Core tracks the entities it returns so it can detect changes and save them. That costs memory and setup. AsNoTracking() skips the change tracker, returning entities faster and lighter.
Use tracking when you'll modify and save the entities. Use AsNoTracking for read-only queries, especially list pages and API reads, where you never write the results back.
// read-only list: no tracking is faster
var products = await _context.Products
.AsNoTracking()
.Where(p => p.InStock)
.ToListAsync();Key point: Volunteering AsNoTracking for read paths signals you've tuned EF Core in production, not just used it.
Eager loading pulls related data in the same query with Include(). Lazy loading fetches related data automatically when you first access the navigation property. Explicit loading loads related data on demand with a manual call.
Eager is usually best because it avoids the N+1 problem, where a loop over parents fires one query per child. Lazy loading is convenient but easy to trigger accidentally in a loop.
// eager loading avoids N+1
var orders = await _context.Orders
.Include(o => o.Lines)
.ThenInclude(l => l.Product)
.ToListAsync();The N+1 problem is when loading a list of N parents triggers one extra query per parent to fetch its children, so you run N+1 queries instead of one or two. It quietly kills performance under load.
The fix is eager loading with Include (or a projection with Select) so the related data comes back in a single round trip. Profiling the SQL EF Core generates is how you catch it.
Key point: If you can name N+1 and show the Include fix, you're ahead of most candidates on the data-access question.
A JWT (JSON Web Token) is a signed token carrying claims about the user. The client sends it in the Authorization header; the JWT bearer middleware validates the signature and expiry, then builds the user identity from the claims.
You configure it with AddAuthentication().AddJwtBearer(...), setting the issuer, audience, and signing key. JWTs are stateless, so the server doesn't store sessions, which suits APIs and microservices.
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = config["Jwt:Issuer"],
IssuerSigningKey = signingKey
};
});The options pattern binds a section of configuration to a strongly typed class, then injects it with IOptions<T>. Instead of reading loose strings from IConfiguration everywhere, you get a typed object with validation.
Use IOptions for values fixed at startup, IOptionsSnapshot for per-request reloads, and IOptionsMonitor for change notifications in singletons. Binding plus validation catches bad config at startup.
public class SmtpOptions { public string Host { get; set; } = ""; public int Port { get; set; } }
builder.Services.Configure<SmtpOptions>(
builder.Configuration.GetSection("Smtp"));
public EmailService(IOptions<SmtpOptions> opts) => _host = opts.Value.Host;ASP.NET Core has a built-in logging abstraction: you inject ILogger<T> and call methods like LogInformation, LogWarning, and LogError. Providers (console, debug, or third-party like Serilog) decide where logs go.
Log levels and filters are set in configuration, so you can raise verbosity in development and quiet it in production. Structured logging (named placeholders) makes logs searchable.
public class ProductsController : ControllerBase
{
private readonly ILogger<ProductsController> _log;
public ProductsController(ILogger<ProductsController> log) => _log = log;
public IActionResult Get(int id)
{
_log.LogInformation("Fetching product {ProductId}", id);
return Ok();
}
}Use exception-handling middleware: UseExceptionHandler points to an error endpoint or a handler that turns exceptions into clean responses. In .NET 8+ you can register an IExceptionHandler for structured handling. This keeps try/catch out of every action.
For APIs, return a consistent ProblemDetails payload so clients get a predictable error shape. UseDeveloperExceptionPage is for development only; never expose stack traces in production.
app.UseExceptionHandler(errApp =>
errApp.Run(async context =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(
new { error = "Something went wrong" });
}));Creating a new HttpClient per call exhausts sockets under load, and a single static HttpClient doesn't pick up DNS changes. IHttpClientFactory manages a pool of handlers with proper lifetimes, fixing both problems.
It also lets you configure named or typed clients with base addresses, default headers, and resilience policies (retries, timeouts) in one place. It's the recommended way to make outbound HTTP calls.
builder.Services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("User-Agent", "my-app");
});
// inject IHttpClientFactory, then:
var client = factory.CreateClient("github");DbContext should be Scoped: one per request. AddDbContext registers it as Scoped by default. A DbContext isn't thread-safe and tracks changes, so sharing one across requests or threads causes concurrency errors and stale tracking.
For background services or parallel work, create a fresh context per unit of work, often via a DbContextFactory. Never make a DbContext a Singleton.
Value types (int, bool, struct, enum) hold their data directly and copy by value on assignment. Reference types (class, string, array, most objects) hold a reference to data on the heap and copy the reference, so two variables can point at the same object.
This shows up in ASP.NET when a mutation to a passed object is visible to the caller, and in nullability: reference types can be null, which is why nullable reference types and null checks matter for avoiding NullReferenceException.
advanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.
The generic host boots the app, builds the DI container, and starts Kestrel. Kestrel accepts a connection, parses the HTTP request into an HttpContext, and runs it through the middleware pipeline. Routing selects an endpoint, the endpoint executes, and the response flows back out.
In front of Kestrel there's often a reverse proxy (IIS with the ASP.NET Core Module, Nginx, or a cloud LB) doing TLS and load balancing. Knowing where forwarded headers, TLS termination, and process lifetime fit is the senior detail.
In classic ASP.NET, blocking on an async call (calling .Result or .Wait() from a request thread) could deadlock because the captured synchronization context needed that same thread to resume. ASP.NET Core has no synchronization context, so that specific deadlock is gone.
The rule still holds: don't block on async code. Use async all the way down. ConfigureAwait(false) matters in libraries to avoid capturing a context, though in ASP.NET Core app code it has little effect since there's no context to capture.
// don't do this: blocks the thread
var data = _repo.GetAsync().Result;
// do this: await all the way down
var data = await _repo.GetAsync();Key point: Saying 'ASP.NET Core removed the sync context, so classic deadlocks are gone, but blocking on async is still wrong' is a precise production-ready answer.
Endpoint routing runs UseRouting to match the request to an endpoint early, stores the chosen endpoint on the HttpContext, then lets middleware between routing and the endpoint (like authorization) inspect metadata such as [Authorize] before the endpoint executes at UseEndpoints or MapControllers.
The split exists so authorization and other middleware can make decisions based on which endpoint was matched without running it. That's why UseAuthorization sits between UseRouting and the endpoint mapping.
Response caching stores full responses keyed by URL and headers, good for anonymous GETs. Output caching (added in .NET 7) is a more controllable server-side cache with tag-based invalidation. IMemoryCache caches arbitrary objects in process; IDistributedCache (Redis, SQL) shares a cache across instances.
The judgment call: in-memory for single-instance or per-node data, distributed for scaled-out apps that must share state, and HTTP response or output caching for cacheable endpoints. Always plan invalidation before adding a cache.
| Cache | Scope | Best for |
|---|---|---|
| IMemoryCache | One process | Fast per-node object caching |
| IDistributedCache | Shared (Redis, SQL) | Multi-instance shared state |
| Output caching | Server-side responses | Cacheable endpoints with tag invalidation |
| Response caching | HTTP layer | Anonymous GETs with cache headers |
Keep the app stateless so any instance can serve any request: no in-memory session tied to one node, no local file state. Move session and cache to a distributed store (Redis), put shared files in blob storage, and let a load balancer spread traffic.
Data protection keys (used for cookies and antiforgery) must be shared across instances too, or cookies issued by one node fail on another. That shared-keyring detail is a common production gotcha worth naming.
When the default binding can't express your input: parsing a composite value from multiple sources, decoding a custom header or token into a rich object, or binding a format the framework doesn't handle. You implement IModelBinder and register it via a provider or [ModelBinder] attribute.
The honest answer includes reaching for simpler options first: a [FromBody] DTO plus a small mapping, or a TypeConverter, often solves the problem without a full custom binder.
Generate migrations in development with dotnet ef migrations add, The generated SQL, and apply them as a deliberate deploy step: an idempotent SQL script run by your pipeline, or a controlled migration bundle, rather than auto-migrating on app startup matters.
For zero-downtime, use expand-and-contract: add nullable columns and new tables first, deploy code that writes both old and new, backfill, then drop the old shape in a later migration. Auto-migrate-on-startup is fine for demos, risky for real traffic.
Use optimistic concurrency: add a rowversion or concurrency token to the entity. EF Core includes it in the UPDATE's WHERE clause, so if another transaction changed the row first, zero rows update and EF throws DbUpdateConcurrencyException, which you catch and resolve.
Resolution options are last-write-wins, first-write-wins, or merging and re-prompting the user. Pessimistic locking (SELECT FOR UPDATE) is the alternative when conflicts are frequent, at the cost of throughput.
public class Product
{
public int Id { get; set; }
public decimal Price { get; set; }
[Timestamp] public byte[] RowVersion { get; set; } = default!;
}
// on save, a stale RowVersion throws DbUpdateConcurrencyExceptionImplement IHostedService or derive from BackgroundService for long-running loops, registered with AddHostedService. The host starts and stops them with the app. Inside, create a DI scope per unit of work to resolve scoped services like DbContext.
For real queues and retries, hand work to a durable queue (a message broker or a library like Hangfire) rather than an in-process loop, because an in-process worker dies with the process and loses unstarted work.
public class CleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopes;
public CleanupService(IServiceScopeFactory scopes) => _scopes = scopes;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using var scope = _scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ShopContext>();
// do work
await Task.Delay(TimeSpan.FromMinutes(5), ct);
}
}
}Add the API versioning package and pick a strategy: URL segment (/v1/products), a query string, or a header. URL versioning is the most visible and cache-friendly; header versioning keeps URLs clean but is harder to test by hand.
The point is a contract: existing clients keep working when you ship v2. Pair versioning with a deprecation policy and clear docs so old versions eventually retire.
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
[HttpGet, MapToApiVersion("2.0")]
public IActionResult GetV2() => Ok();
}gRPC uses HTTP/2 and Protocol Buffers, giving you a strict contract, smaller payloads, streaming, and lower latency, which suits service-to-service calls inside your own system. REST with JSON wins for public APIs, browser clients, and easy human debugging.
The trade-off is reach and tooling: gRPC needs code generation and isn't natively callable from browsers without gRPC-Web, while REST works everywhere. Pick gRPC for internal high-throughput links, REST for broad external consumption.
| gRPC | REST (JSON) | |
|---|---|---|
| Transport | HTTP/2 | HTTP/1.1 or 2 |
| Payload | Protobuf (binary) | JSON (text) |
| Contract | Strict (.proto) | Loose (OpenAPI optional) |
| Best for | Internal service-to-service | Public and browser clients |
SignalR is ASP.NET Core's library for real-time, two-way communication between server and clients: chat, live dashboards, notifications. Clients to a hub, and the server can push messages connects to one client, a group, or everyone.
It negotiates the best transport, preferring WebSockets and falling back to server-sent events or long polling. For scale-out across servers, you add a backplane (Redis or Azure SignalR) so a message on one node reaches clients connected to another.
Three pillars: structured logging (ILogger with a sink like Serilog or OpenTelemetry), metrics (request rate, latency, error rate via OpenTelemetry or Prometheus), and distributed tracing so a request can be followed across services. Add health checks (AddHealthChecks) for liveness and readiness probes.
The goal is to answer 'is it up, is it slow, and why' without redeploying. Correlation IDs threaded through logs and traces are what tie a single user's request together across the system.
builder.Services.AddHealthChecks()
.AddDbContextCheck<ShopContext>();
app.MapHealthChecks("/health/live");
app.MapHealthChecks("/health/ready");Parameterized queries or EF Core stop SQL injection; Razor's automatic encoding and a Content Security Policy blunt XSS; antiforgery tokens defend cookie-based forms against CSRF; enforce HTTPS with HSTS; and validate and bind input with DTOs to prevent over-posting.
Beyond code: keep secrets out of the repo (secret managers, not appsettings), patch dependencies, set security headers, and apply least-privilege on the database account. Naming the attack and its specific mitigation is what this question checks.
A middleware short-circuits by writing a response and not calling the next delegate, so nothing further in the pipeline runs. Rate limiting, a cached-response hit, an auth rejection, or a maintenance gate all short-circuit deliberately.
The care point is ordering and cleanup: short-circuiting before authentication skips it entirely, and any using or logging that wraps next won't see the request the way you expect. Short-circuit on purpose, near the top, with a clear response.
app.Use(async (context, next) =>
{
if (IsBlocked(context.Request))
{
context.Response.StatusCode = 429; // no next(): pipeline stops here
await context.Response.WriteAsync("Too many requests");
return;
}
await next();
});Unit-test business logic in isolation with a framework like xUnit and mocks for dependencies. Integration-test the real pipeline with WebApplicationFactory<T>, which spins up an in-memory server so you can send HTTP requests and assert on responses, routing, auth, and serialization end to end.
The judgment is where to draw the line: mock external boundaries (payment gateways, email), but use a real or in-memory database for integration tests so EF Core mappings and migrations are exercised. Test behavior, not implementation.
public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductsApiTests(WebApplicationFactory<Program> f) => _client = f.CreateClient();
[Fact]
public async Task Get_unknown_returns_404()
{
var resp = await _client.GetAsync("/api/products/999999");
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
}Key point: Naming WebApplicationFactory for integration tests signals you've tested the real pipeline, not just mocked units.
Observe first: check logs, metrics, and traces around the slow or failing path, and correlate with recent deploys. Is it CPU, memory, blocked threads, or waiting on a downstream (database, external API)? Thread-pool starvation from blocking async is a common culprit; N+1 queries and missing timeouts are others.
Then reproduce at a smaller scale, form a hypothesis, fix at the right layer, and confirm with a measurement. The structure, observe, hypothesize, verify, fix, confirm, matters more than any single tool.
Key point: wearing an ASP.NET costume. The methodology is; tools support the evidence.
ASP.NET wins when a team already works in C# and the .NET ecosystem and wants a typed, tooled framework with first-class async and built-in dependency injection. Within .NET itself, the real choice is ASP.NET Core versus the older .NET Framework ASP.NET: Core is cross-platform, faster, and where all new development goes, while the old stack stays only for legacy Windows apps. Against other language ecosystems, ASP.NET trades some of Node's flexibility and startup speed for strong typing, a mature runtime, and predictable performance under load. Saying these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.
| Framework | Language | Platforms | Best at | Watch out for |
|---|---|---|---|---|
| ASP.NET Core | C# | Windows, Linux, macOS | New web apps and APIs, high throughput | Learning curve if new to .NET |
| ASP.NET (.NET Framework) | C# | Windows only | Maintaining legacy enterprise apps | No cross-platform, in maintenance mode |
| Node.js (Express) | JavaScript | Cross-platform | Fast startup, huge npm ecosystem | Dynamic typing, callback and async pitfalls |
| Spring Boot | Java | Cross-platform | Large enterprise JVM systems | Verbosity, heavier configuration |
Prepare in layers, and practice out loud. Most ASP.NET rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical ASP.NET interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These ASP.NET questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works