Part 6: ASP.NET Core Web Development – C# / .NET Interview Questions and Answers

This chapter explores ASP.NET Core Web Development.
The Answers are split into sections: What πΌ Junior, π Middle, and π Senior .NET engineers should know about a particular topic.
Also, please take a look at other articles in the series: C# / .NET Interview Questions and Answers
Fundamentals
This section introduces its core concepts, architecture, and differences from older ASP.NET versions, setting the stage for deeper topics ahead.

β What is ASP.NET Core, and how does it differ from the old ASP.NET (Framework)?
ASP.NET Core is a modern, cross-platform, open-source web framework thatβs part of the unified .NET platform. Unlike ASP.NET (Framework), which is primarily hosted on IIS and is tied to Windows, ASP.NET Core runs on Kestrel across Windows, Linux, and macOSβoften behind a reverse proxy (IIS/NGINX/Apache) in production. Its modular architecture (no System.Web) and redesigned pipeline deliver notably higher throughput and lower allocations
Feature | ASP.NET (Framework) | ASP.NET Core |
---|---|---|
Platform | Windows-only | Cross-platform (Windows/Linux/macOS) |
Hosting | IIS-only | Kestrel, IIS, Nginx, etc. |
Performance | Slower, more overhead | High-performance, redesigned pipeline; supports Minimal APIs |
Architecture | Tightly coupled system.web | Modular, no system.web |
Dependency Injection | Manual or third-party only | Built-in DI |
Versioning | Part of .NET Framework | Part of .NET (Core/5+) |
Open Source | Reference source available, but not fully community-driven | Fully open source on GitHub |
What .NET engineers should know:
- πΌ Junior: Should know that ASP.NET Core is the newer, cross-platform version and is recommended for new projects.
- π Middle: Expected to work fluently with ASP.NET Core, understand middleware, dependency injection, hosting models, and build REST APIs.
- π Senior: Should know the architectural differences, migration paths from legacy ASP.NET, performance tuning in Core, and how to design scalable apps using modern hosting models (Kestrel + reverse proxy, containers, etc.).
π Resources:
β What is Kestrel, and can ASP.NET Core run without IIS?
Kestrel is the default cross-platform web server for ASP.NET Core. Itβs a lightweight, high-performance HTTP server built into the framework. Yes, ASP.NET Core apps can run without IIS β in fact, Kestrel is often used as the web server on all platforms. On Windows, production hosting is typically IIS (full) acting as a reverse proxy to Kestrel. On Linux, you can run Kestrel directly or place NGINX/Apache/Traefik in front; choose based on TLS/process mgmt/security needs. This cross-platform web server approach is a departure from the old ASP.NET, which was tightly integrated with IIS.
ASP.NET Core project templates use Kestrel by default when not hosted with IIS. In the following template-generated Program.cs
, the WebApplication.CreateBuilder
method calls UseKestrel internally:
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
options.AddServerHeader = false;
});
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
What .NET engineers should know about Kestrel:
- πΌ Junior: Should know Kestrel is the web server that runs ASP.NET Core apps, and it doesn't require IIS.
- π Middle: Should understand how to configure Kestrel, when to use IIS or Nginx as a reverse proxy, and how SSL/offloading works.
- π Senior: Should architect production setups with Kestrel + reverse proxy, handle edge cases (timeouts, limits), and understand server tuning for high-performance APIs.
π Resources:
βHow does the ASP.NET Core request processing pipeline work, and what is middleware?
In ASP.NET Core, HTTP requests flow through a sequence of middleware components, which together form the request pipeline. Each middleware can handle the request and/or response, and then optionally pass control to the next middleware in the pipeline. Middleware is essentially a modular handler that you configure in code (typically in Program.cs or Startup.cs). For example, there are built-in middleware options for routing, authentication, and serving static files, among others, and you can also insert custom ones.

app.UseRouting(); // Sets up routing
app.UseAuthentication(); // Checks for auth
app.UseAuthorization(); // Checks access
app.MapControllers(); // Executes controller action
The pipeline is executed in order; a middleware can perform work before and after calling the next one.
What .NET engineers should know about middleware:
- πΌ Junior: Should understand that middleware is like a chain of handlers for HTTP requests and responses. Should know how to add and use existing middleware like
UseRouting
,UseAuthentication
. - π Middle: Should be able to write custom middleware, understand how early termination works, and manage orders for correct behavior (e.g., exception handling before routing).
- π Senior: Should architect pipelines for cross-cutting concerns (logging, error handling, metrics), optimize middleware performance, and debug complex request flows.
π Resources: ASP.NET Core Middleware
β How do you create a custom middleware in ASP.NET Core?
Creating your middleware in ASP.NET Core enables you to plug custom logic into the request pipeline, such as logging, timing, headers, or metrics.
There are two main ways to write middleware:
Option 1: Use inline in Program.cs
app.Use(async (context, next) =>
{
Console.WriteLine($"Request: {context.Request.Method} {context.Request.Path}");
await next(); // Call the next middleware
});
Option 2: Create a middleware class
namespace Middleware.Example;
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}
// IMessageWriter is injected into InvokeAsync
public async Task InvokeAsync(HttpContext httpContext, IMessageWriter svc)
{
svc.Write(DateTime.Now.Ticks.ToString());
await _next(httpContext);
}
}
public static class MyCustomMiddlewareExtensions
{
public static IApplicationBuilder UseMyCustomMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyCustomMiddleware>();
}
}
// in program.cs
app.UseMyCustomMiddleware();
What .NET engineers should know:
- πΌ Junior: Should know how to write a basic inline middleware using
app.Use
. - π Middle: Should be able to build reusable middleware classes, manage execution order, and handle exceptions safely.
- π Senior: Should be able to write cross-cutting middleware (correlation ID, telemetry, auth wrappers), handle edge cases (async disposal, scoped services), and understand performance implications.
β Whatβs the difference between app.Use()
, app.Run()
, and app.Map()
in the middleware pipeline?
In ASP.NET Core, these three methods define how middleware behaves β each serves a different purpose in building the request pipeline.
app.Use()
- Most commonly used.
- Allows chaining middleware via the
next()
method. - You can perform actions before and after calling the next middleware.
app.Run()
- Ends the pipeline. No
next()
. - Think of it as a terminal middleware.
- Use it for simple early termination.
app.Map()
- Branching based on request path.
- Only runs its pipeline if the path matches.
- Helpful in splitting pipelines.

Summary:
Method | Calls next() ? | Ends pipeline? | Conditional (by path)? |
---|---|---|---|
Use | β Yes | β No | β No |
Run | β No | β Yes | β No |
Map | β Inside branch | β No | β Yes |
What .NET engineers should know:
- πΌ Junior: Should know that
Use()
chains middleware,Run()
ends it, andMap()
splits it by route. - π Middle: Should structure pipelines properly using all three, avoid unreachable middleware, and debug execution order.
- π Senior: Should be able to use
MapWhen()
andMap()
for branching, support multi-tenant or plugin-style routing, and isolate pipeline concerns like metrics, health checks, and APIs.
β What is routing in ASP.NET Core, and how is it configured?
Routing in ASP.NET Core is the system that maps incoming HTTP requests to the correct endpoint β usually a controller action, Razor page, or minimal API.
Routing options:
Minimal APIs / Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello World!");
app.Run();
With Controllers:
// Program.cs
builder.Services.AddControllers();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
Controller:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id) => Ok($"User #{id}");
}
How is routing configured?
Option 1: Attribute routing (most common)
Use [Route]
and [HttpGet]
, [HttpPost]
, etc., to define paths directly on actions.
Option 2: Convention-based routing (less standard in Core)
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
Route patterns support:
{parameter}
for capturing values{parameter?}
for optional values- Constraints like
{id:int}
or{slug:alpha}
What .NET engineers should know:
- πΌ Junior: Should understand how routes match URLs to controller actions or endpoints. Use
[Route]
andMapGet()
. - π Middle: Should define flexible route patterns, support multiple routes per action, and apply route constraints or defaults.
- π Senior: Should be aware of route versions (e.g.,
api/v1
), build custom route conventions, apply endpoint filters/middleware, and debug routing issues in complex systems.
π Resources:
β How does the ASP.NET Core request lifecycle work from request to response?
The ASP.NET Core request lifecycle is the journey an HTTP request takes through the framework, from the moment it enters Kestrel to when the response is sent back. Understanding it helps you decide where to put cross-cutting logic (middleware, endpoint filters, MVC filters) and how different components interact.
Key stages:
- Kestrel β The built-in web server receives the HTTP request and passes it to the hosting layer.
- Middleware pipeline β Executes in the order you register (
app.Use...
), can short-circuit, modify requests/responses, or delegate to the next. Global concerns like logging, exception handling, auth, and CORS live here. - Routing β Matches the request URL/HTTP method to an endpoint, if none match - 404.
- Endpoint filters (.NET 7+) β Run after routing but before endpoint execution; work for minimal APIs and MVC endpoints. Useful for lightweight per-endpoint validation, shaping, or logging.
- MVC filter pipeline β If MVC is selected, filters run in this order:
- Authorization filters: check user access before hitting the action.
- Resource filters: run before/after model binding.
- Action filters: wrap the action method execution.
- Result filters: wrap the result execution (e.g., modify the response).
- Exception filters: handle unhandled exceptions inside MVC.
- Action method execution β Your controller or page handler runs.
- Result execution β Response is prepared (JSON serialization, Razor view rendering, file streaming).
- Response pipeline β Middleware after the endpoint can still modify the outgoing response.
- Kestrel sends a response β Data goes back to the client.
What .NET engineers should know:
- πΌ Junior: Requests pass through middleware first, then endpoint filters (if any), then MVC filters/actions. Order matters.
- π Middle: Understand early termination; know which stage to hook into for logging, caching, auth, validation.
- π Senior: Understand properly how the pipeline works, can operate efficiently with different elements from the pipeline, and choose correctly which part of the code should be placed in which pipeline stages.
π Resources:
β How to host and deploy ASP.NET Core cross-platform?
ASP.NET Core runs on Windows, Linux, and macOS, giving you multiple hosting options:
- Self-contained β app ships with .NET runtime, no need to install it on the server.
- Framework-dependent β smaller publish, requires runtime installed.
- Docker containers β portable, great for cloud deployments.
- Reverse proxy β Nginx (Linux) or IIS (Windows) for SSL, load balancing.
What .NET engineers should know:
- πΌ Junior: Know .NET apps can run on different OSes without changing code.
- π Middle: Understand self-contained vs framework-dependent, and how to use Nginx/IIS as a reverse proxy.
- π Senior: Design automated CI/CD pipelines, containerize for Kubernetes, and optimize cross-platform performance.
π Resources:
Middleware & Routing
ASP.NET Core processes HTTP requests through a configurable middleware pipeline. Here, we explore how middleware works, how routing determines request destinations, and how to customize these behaviors.

β How to implement global error handling, and why prefer UseExceptionHandler
middleware over exception filters?
Global error handling should sit in the middleware pipeline so it catches exceptions from any endpoint (such as minimal APIs, MVC, Razor Pages, static files, gRPC negotiations, etc.). UseExceptionHandler
runs early and consistently; MVC exception filters only run inside the MVC filter pipeline (after routing and only for controller/page actions). Thatβs why UseExceptionHandler
is the recommended global approach.
// Program.cs β production-safe global handler with ProblemDetails
using Microsoft.AspNetCore.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(); // .NET 7/8 built-in ProblemDetails service
var app = builder.Build();
// 1) Global handler (before auth/authorization)
app.UseExceptionHandler("/error");
// 2) Optional: surface 4xx as text/json in non-dev
// app.UseStatusCodePages();
app.MapGet("/boom", () => throw new InvalidOperationException("Oops"));
// Centralized error endpoint β works for ANY exception in the app
app.Map("/error", (HttpContext http) =>
{
var feature = http.Features.Get<IExceptionHandlerFeature>();
var ex = feature?.Error;
// log with your ILogger here if needed
var traceId = http.TraceIdentifier;
return Results.Problem(
title: "Unhandled server error",
detail: ex?.Message,
statusCode: StatusCodes.Status500InternalServerError,
instance: http.Request.Path,
extensions: new Dictionary<string, object?> { ["traceId"] = traceId });
});
app.Run();
Why UseExceptionHandler
over exception filters?
- Scope: middleware wraps the whole app; filters only wrap MVC actions.
- Consistency: handles failures that occur before MVC (model binding selection, endpoint selection for minimal APIs, file endpoints, etc.).
- Simplicity: one place to produce ProblemDetails, set headers, and log.
- Compatibility: works the same whether you use minimal APIs or MVC; filters donβt run for minimal APIs.
- Security: easy to keep detailed errors off in prod (pair with
DeveloperExceptionPage
only in dev).
What .NET engineers should know:
- πΌ Junior: Able to put error handling in middleware so it catches everything;
- π Middle: Know how to log exceptions with correlation IDs, map known exceptions to proper status codes (e.g., 400/404), and keep messages safe (no PII).
- π Senior: Able to handle errors across services, add exception-to-HTTP mapping, integrate with observability (trace/span IDs), and ensure idempotent error responses during retries.
π Resources:
β How to serve static files in ASP.NET Core and configure caching headers for them?
Kestrel serves static files (JS, CSS, images) via the static-file middleware. By default, it serves from the wwwroot folder. You can add more file providers and set cache headers per path to control browser/CDN caching.

Example set Cache-Control headers globally for static files:
using Microsoft.Net.Http.Headers;
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Example: 7 days for all static files
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(7)
};
}
});
HTML:
<!-- Razor view tip β automatic cache-busting for files in wwwroot -->
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<script src="~/js/site.js" asp-append-version="true"></script>
What .NET engineers should know:
- πΌ Junior: Should be able to put files in
wwwroot
, enableUseStaticFiles()
, and set Cache-Control for better performance. Useasp-append-version
to bust caches when files change. - π Middle: Apply different cache policies by path; use long
max-age
+immutable
for fingerprinted assets, short TTLs for others. Remember static middleware supports conditional requests viaLast-Modified
. - π Senior: Pair caching with a CDN, negotiate compression (ResponseCompression), and keep static files behind a reverse proxy (Nginx/Apache). Define environment-specific TTLs, ensure deterministic asset hashing in your build, and monitor cache hit ratios.
π Resources:
β What is endpoint routing, and how does it improve performance compared to the old routing system?
Endpoint routing centralizes routing so that the framework selects the endpoint early in the middleware pipeline and attaches it to the HttpContext. This means middleware later in the pipeline already knows which endpoint will run and can make decisions accordingly.
In the old routing system (pre-2.2 MVC routing), routing happened inside MVC after the middleware pipeline ran, so that middleware couldnβt use route metadata without extra complexity.
Performance & flexibility benefits:
- Early endpoint selection reduces work later in the pipeline and allows middleware to skip processing if the route doesnβt match.
- Single unified routing for minimal APIs, MVC, Razor Pages, gRPC, SignalR, etc., no separate routing systems.
- Metadata-driven middleware policies (auth, CORS, filters, rate limits, output caching) can run based on endpoint attributes.
- Lower allocations compared to older MVC routing when using high-throughput endpoints.
Example:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Endpoint routing
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/hello/{name}", (string name) => $"Hello {name}!");
endpoints.MapControllers();
});
app.Run();
What .NET engineers should know:
- πΌ Junior: Should know that endpoint routing picks the route before MVC runs; this helps middleware see where the request is going.
- π Middle: Understand that endpoint metadata drives per-endpoint policies (auth, CORS, rate limits). It unifies routing for all frameworks in ASP.NET Core.
- π Senior: Use endpoint metadata for performance tuning and fine-grained policies. Design pipelines where middleware can short-circuit early based on endpoint info, and avoid unnecessary work for unmatched routes.
π Resources:
Hosting Models & Application Startup
This section explains hosting options, startup flow, and service registration.

β What is the "minimal hosting model" introduced in .NET 6, and how does it differ from the previous Startup.cs model?
In .NET 6, Microsoft introduced the minimal hosting model β a more straightforward, cleaner way to build ASP.NET Core apps by ditching the old Startup.cs
+ Program.cs
split.
Instead of multiple files and boilerplate, you configure everything in a single file: Program.cs
.
Minimal hosting model (in .NET 6+)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Old model (before .NET 6, but still supported)
// Program.cs
public class Program
{
public static void Main(string[] args)
=> CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
// Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services) { }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { }
}
Key differences between minimal hosting and startup.cs:
Feature | Minimal Hosting (from .NET 6) | Traditional Startup.cs |
---|---|---|
Files | Single (Program.cs ) | Two (Program.cs + Startup.cs ) |
API style | Fluent, top-level statements | Structured methods |
Flexibility | More dynamic | More structured |
Learning curve | Easier for small projects | Better for large separation |
What .NET engineers should know:
- πΌ Junior: Should understand the basic layout of a minimal API in
Program.cs
and how services and middleware are registered. - π Middle: Expected to migrate apps from Startup.cs to minimal hosting, understand lifecycle, dependency injection, and modularization in the new style.
- π Senior: Should design scalable app architectures using minimal APIs, and organize large projects without a Startup.cs, and decide when the classic pattern is a better fit (e.g., enterprise apps with strict layering).
π Resources: Minimal APIs overview
β How do you configure services and the HTTP request pipeline in ASP.NET Core?
In ASP.NET Core, there are two phases in application startup:
- Configure Services β this is where you register dependencies.
- Configure the HTTP request pipeline β this is where you define how the app handles incoming requests (via middleware).
In the minimal hosting model (Program.cs
)
var builder = WebApplication.CreateBuilder(args);
// Configure services (Dependency Injection)
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddScoped<IMyService, MyService>();
var app = builder.Build();
// Configure HTTP request pipeline (Middleware)
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
What's happening behind the scenes:
builder.Services
is the built-in DI container. You register things here once, and theyβre injected wherever needed.app.UseXYZ()
adds middleware components β logging, routing, auth, etc.app.MapControllers()
connects the pipeline to controller endpoints.
Typical pipeline order:
app.UseExceptionHandler(); // Global error handling
app.UseHttpsRedirection(); // Force HTTPS
app.UseRouting(); // Enable routing
app.UseAuthentication(); // Auth middleware
app.UseAuthorization(); // Permission checks
app.MapControllers(); // Endpoint execution
What .NET engineers should know:
- πΌ Junior: Should know the difference between
builder.Services
(setup) andapp.UseXYZ()
(request pipeline). Should be able to add basic services and middleware. - π Middle: Should organize service registrations using extension methods, group middleware properly, and debug pipeline issues.
- π Senior: Should design modular service configuration, apply cross-cutting concerns (logging, metrics, CORS), optimize middleware performance, and manage scopes/lifetimes correctly.
π Resources:
β What is the difference between a Generic Host and a Web Host in ASP.NET Core?
The Generic Host (Host.CreateDefaultBuilder
) is the unified hosting model introduced in ASP.NET Core 2.1+ that can host any .NET app β web apps, background services, worker services, etc. The Web Host (WebHost.CreateDefaultBuilder
) was the original host for ASP.NET Core web apps only, and itβs now considered legacy for new apps.
Aspect | Generic Host (IHost ) | Web Host (IWebHost ) |
---|---|---|
Scope | Any .NET Core app (web, gRPC, background, etc.) | Web apps only |
Abstraction | IHost , IHostBuilder | IWebHost , IWebHostBuilder |
Service container | Unified DI for all workloads | Web DI only |
Background tasks | Supports IHostedService natively | Requires hacks or separate services |
Config & logging | Unified config/logging setup via HostBuilder | Web-focused config/logging |
Status | Recommended for all new projects | Legacy for backward compatibility |
Example β Generic Host (modern)
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((ctx, services) =>
{
services.AddHostedService<MyBackgroundService>();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
var app = builder.Build();
await app.RunAsync();
What .NET engineers should know:
- πΌ Junior: Generic Host is newer and works for all app types; Web Host is old and only for web apps.
- π Middle: Generic Host adds flexibility β you can run web + background services together with the same DI container and config.
- π Senior: Use Generic Host for future-proofing; know how to configure multiple hosted services, integrate non-web workloads, and migrate from Web Host if maintaining older apps.
π Resources: .NET Generic Host in ASP.NET Core
β What are IHostedService
and BackgroundService
, and when should you use them?
IHostedService
is the ASP.NET Core abstraction for long-running background tasks that start with the app and stop on shutdown. BackgroundService
is an abstract base class implementing IHostedService
that gives you a dedicated ExecuteAsync
method, making it more straightforward to write background loops without boilerplate.
When to use:
- For tasks that run alongside your app but arenβt triggered by HTTP requests.
- Examples: message queue consumers, periodic cleanup jobs, cache warming, metrics pushers.
- Use
IHostedService
For complete control (start/stop events); useBackgroundService
for simpler continuous or periodic work.
Example β IHostedService (manual start/stop):
public class MyWorker : IHostedService
{
private Timer? _timer;
public Task StartAsync(CancellationToken ct)
{
_timer = new Timer(_ => Console.WriteLine("Tick"), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct)
{
_timer?.Dispose();
return Task.CompletedTask;
}
}
What .NET engineers should know:
- πΌ Junior: Both are for background work in ASP.NET Core.
BackgroundService
is a simpler base class for loops. - π Middle: Pick
IHostedService
when you need fine-grained start/stop control;BackgroundService
for continuous tasks. Always honor cancellation tokens for graceful shutdown. - π Senior: Use DI for dependencies, ensure idempotent operations, add error handling/retries, and consider external schedulers (Quartz.NET, Hangfire, Kubernetes CronJobs) for durable or critical jobs.
β IHostedService
vs BackgroundService
vs schedulers β when to choose what?
Option | What it is | Best for | Pros | Cons | Typical setup |
---|---|---|---|---|---|
IHostedService | Low-level start/stop interface for background work | Precise lifecycle control, one-off startup/teardown tasks | Minimal deps; full control over Start/Stop | More boilerplate; you manage loops/timers | services.AddHostedService<MyWorker>() |
BackgroundService | Base class that gives you ExecuteAsync loop | Continuous or periodic tasks in-process | Simple loop model; honors cancellation; easy DI | In-process only; restarts lose in-memory state | class MyWorker : BackgroundService { β¦ } |
Quartz.NET | In-process scheduler with cron, calendars, persistence | Rich schedules, many jobs, clustering (DB-backed) | Cron, misfire handling, durable stores | Extra package; DB setup; operational overhead | services.AddQuartz().AddQuartzHostedService() |
Hangfire | In-process/out-of-process background jobs in DB queue | Fire-and-forget, delayed, recurring jobs with retries | Dashboard; retries; persistence; scale-out | Requires DB/Redis; licensing for some features | services.AddHangfire(...); app.UseHangfireDashboard() |
Azure Functions (Timer) | Serverless scheduled functions | Cloud-native schedules, pay-per-use | No servers; easy ops; scale | Azure-only; cold starts; vendor lock-in | function.json with schedule |
Kubernetes CronJob | Cluster-level scheduled pods | Heavy jobs, infra tasks, language-agnostic | Strong isolation; infra-grade reliability | Requires k8s; separate deployment | CronJob manifest with cron |
What .NET engineers should know about IHostedService
vs BackgroundService
:
- πΌ Junior: Should use
BackgroundService
for simple loops;IHostedService
when you need a custom start/stop, for cron-like tasks, prefer a scheduler (Quartz/Hangfire). - π Middle: Should be able to persist state for critical work; add retries/backoff; ensure graceful shutdown via cancellation tokens; consider Hangfire/Quartz when jobs must survive restarts.
- π Senior: Know and be able to choose external schedulers (Hangfire/Quartz/Azure/K8s) when its needed for durability, observability, and scale-out. Make jobs idempotent, add metrics/tracing, and define SLAs and failure policies.
π Resources:
- CronJob k8s
- Timer trigger for Azure Functions
- hangfire
- Quartz.NET
- Background tasks with hosted services in ASP.NET Core
Dependency Injection & Configuration
ASP.NET Coreβs built-in dependency injection simplifies service management and testing. Here, we discuss DI fundamentals, advanced registration patterns, and configuration handling best practices.

β How does built-in Dependency Injection (DI) work in ASP.NET Core, and what are service lifetimes?
ASP.NET Core has built-in dependency injection (DI) β no need for third-party containers. Itβs how you manage object creation and pass dependencies automatically where needed.
You register services once in Program.cs
, and the framework injects them into controllers, services, or middleware.
ASP.NET Core supports three built-in lifetimes:
Lifetime | Description | Example use |
---|---|---|
Singleton | One instance for the entire app lifetime | Configuration, caching |
Scoped | One instance per HTTP request | Repositories, services |
Transient | A new instance every time it's requested | Lightweight, stateless helpers |
Image below describe how scopes and lifetime management happends for those 3:


What .NET engineers should know:
- πΌ Junior: Should know how to register and inject services using constructor injection. Understand the basics of Singleton, Scoped, and Transient.
- π Middle: Should pick the right lifetime for each use case, debug DI-related issues, and organize service registration using extension methods or modules.
- π Senior: Should manage complex DI graphs, resolve lifetime mismatches, design for testability, and apply best practices like avoiding service locator patterns or over-injection.
π Resources:
- Dependency injection in ASP.NET Core
- .NET dependency injection
- ASP.NET Core Blazor dependency injection best practices
β Which DI antipatterns do you know in ASP.NET Core?
Here are common dependency injection antipatterns
β 1. Service Locator
Hides dependencies instead of injecting them explicitly. Breaks testability, readability, and maintainability.
var myService = serviceProvider.GetService<IMyService>();
π‘ Instead: use constructor injection.
β 2. Captive Dependency
A long-lived service (e.g., Singleton) depends on a short-lived one (e.g., Scoped). Causes bugs and scope-related memory leaks.
builder.Services.AddSingleton<ServiceA>();
builder.Services.AddScoped<IDbContext, DbContext>();
π‘ Rule: scoped should never be injected into a singleton.
β 3. Disposing of Transient Services Manually
ASP.NET Core disposes of services it creates β donβt manually dispose of them unless you own their lifecycle.
public class MyService : IDisposable
{
public void Dispose() => Console.WriteLine("Disposing manually");
}
π‘ Let the container handle cleanup.
β 4. Async Factories in DI
Async constructors arenβt supported. Trying to use async setup during injection causes deadlocks or async void chaos.
public MyService()
{
var result = AsyncMethod().GetAwaiter().GetResult(); // deadlock alert
}
π‘ Use IHostedService or init methods instead.
β 5. Service Injection into ViewModels or Entities
Don't inject services into models or entities β it couples business data to infrastructure logic.
β 6. Over-injection
Passing too many services into a single class is a smell. Often, it means the class is doing too much.
public class GodService(
IUserService u, IEmailService e, ICache c, ILogger l, IMapper m) { }
β 7. Hidden DI in static classes
Static classes can't take advantage of DI and make unit testing a nightmare.
π‘ Prefer injecting dependencies into instance-based classes.
What .NET engineers should know:
- πΌ Junior: Should avoid service locator, prefer constructor injection, and understand basic lifetimes.
- π Middle: Expected to spot captive dependencies, avoid over-injection, and understand async pitfalls in DI.
- π Senior: Should refactor to avoid DI smells, guide others away from hidden coupling, and architect scalable, testable service registration patterns.
π Resources: Dependency injection guidelines
β Can you describe use cases for [FromKeyedServices]
attribute in DI?
[FromKeyedServices]
is a feature in .NET 8 that lets you inject named (keyed) services into minimal APIs, controllers, or other endpoints. Itβs like telling the DI container:
"Give me this specific version of the service by key."
It's perfect when you have multiple implementations of the same interface and want to resolve the correct one by name, not by type.
Example use case: Multiple payment providers
Imagine you have two implementations of IPaymentService
:
builder.Services.AddKeyedScoped<IPaymentService, StripePaymentService>("stripe");
builder.Services.AddKeyedScoped<IPaymentService, PayPalPaymentService>("paypal");
Then, in your minimal API, you can select one like this:
app.MapPost("/pay/stripe", ([FromKeyedServices("stripe")] IPaymentService service) =>
{
return service.Process();
});
app.MapPost("/pay/paypal", ([FromKeyedServices("paypal")] IPaymentService service) =>
{
return service.Process();
});
When to use [FromKeyedServices]
:
Scenario | Why it fits |
---|---|
Multiple handlers for different strategies | Inject by scenario-specific key |
Multitenant support (per-tenant logic) | Inject services per tenant key |
Feature toggles or config-based wiring | Load services by config key |
APIs that expose different behaviors | Clean separation in endpoints |
Manual resolution (non-attribute approach)
If you need to resolve manually:
var service = context.RequestServices.GetKeyedService<IPaymentService>("stripe");
But using [FromKeyedServices]
is cleaner and more idiomatic for modern APIs.
What .NET engineers should know:
- πΌ Junior: Might not use
[FromKeyedServices]
in the right away, but should know it's useful when the same interface has multiple flavors. - π Middle: Should be able to register and inject keyed services properly, using them for API branching or strategy patterns.
- π Senior: Should design modular systems where keyed DI avoids bloated factories or service locators, and helps cleanly decouple behavior.
π Resources: Keyed services in C#
β How is configuration handled in ASP.NET Core?
ASP.NET Core has a flexible configuration system that loads settings from multiple sources β JSON files, environment variables, command-line args, and secrets, then merges them into a single configuration object.
Below are the available options to handle configuration:
1. appsettings.json & environment-specific files
By default, the app loads:
appsettings.json
(base settings)appsettings.Development.json
,appsettings.Production.json
, etc. (environment overrides)
// appsettings.json
{
"AppSettings": {
"SiteName": "My Site",
"EnableFeatureX": true
}
}
var builder = WebApplication.CreateBuilder(args);
var siteName = builder.Configuration["AppSettings:SiteName"];
2. Environment-based configuration
The environment is set via DOTNET_ENVIRONMENT
(or ASPNETCORE_ENVIRONMENT
for web apps).
Common values: Development
, Staging
, Production
.
if (builder.Environment.IsDevelopment())
{
// Dev-only configuration
}
3. IOptions pattern (strongly typed settings)
For type-safe access to config sections:
Register the settings class:
builder.Services.Configure<AppSettings>(
builder.Configuration.GetSection("AppSettings"));
Inject with IOptions:
public class HomeController : Controller
{
private readonly AppSettings _settings;
public HomeController(IOptions<AppSettings> options)
{
_settings = options.Value;
}
}
4. Reloading configuration at runtime
If reloadOnChange: true
(default), updating appsettings.json
automatically updates values for services using IOptionsSnapshot
.
5. Other configuration sources
- Environment variables (highest precedence after code)
- User secrets (for development only, via
dotnet user-secrets
) - Azure Key Vault or other providers
- Command-line args (override everything else)
What .NET engineers should know:
- πΌ Junior: Should know how to read values from
appsettings.json
and use environment-specific overrides. - π Middle: Should apply the IOptions pattern, understand
IOptionsSnapshot
vsIOptionsMonitor
and structure the config cleanly. - π Senior: Should design multi-source configuration (secrets, vaults, env vars), manage precedence, and support dynamic reloads in production.
π Resources:
- Configuration in ASP.NET Core
- Options pattern in ASP.NET Core
- Safe storage of secrets during development
β What advanced DI features exist (TryAdd
, Replace
, Remove
), and when should you use them?
ASP.NET Coreβs DI container includes handy βextensibilityβ helpers for safe defaults, swapping implementations, and pruning registrations.
TryAdd*
β register only if nothing is registered yet (great for library defaults).TryAddEnumerable
β add another implementation to a multi-registration only if that exact pair isnβt already present.Replace
β swap an existing service descriptor with a new one (override package/library defaults).RemoveAll
β remove all registrations for a given service (clean the slate before custom wiring).
Example:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
// 1) Safe defaults: TryAdd / TryAddSingleton|Scoped|Transient
services.TryAddSingleton<IFoo, DefaultFoo>(); // only if IFoo not already registered
// 2) Multi-impl lists: TryAddEnumerable (no accidental duplicates)
services.TryAddEnumerable(ServiceDescriptor.Transient<IBar, BarA>());
services.TryAddEnumerable(ServiceDescriptor.Transient<IBar, BarB>());
// If BarA added elsewhere, TryAddEnumerable wonβt duplicate it
// 3) Replace an existing registration (override a default)
services.Replace(ServiceDescriptor.Scoped<IFoo, BetterFoo>());
// 4) Remove all implementations for a service (then re-add what you want)
services.RemoveAll<IFoo>();
services.AddScoped<IFoo, FinalFoo>();
When to use each
- Use
TryAdd
in reusable libraries/extensions to supply sensible defaults without blocking app authors who want to override. - Use
TryAddEnumerable
for pipeline-style or composite patterns (e.g., multipleIConfigureOptions<T>
, validators, handlers) to avoid duplicate entries. - Use
Replace
when a package registers something you want to override globally (e.g., swap a clock, serializer, or strategy). - Use
RemoveAll
when you need a clean slate before applying your own set (e.g., wiping default handlers or test doubles in integration tests).
What .NET engineers should know about advanced DI features:
- πΌ Junior: Should know following:
TryAdd
avoids overwriting existing registrations;Replace
overrides;RemoveAll
clears. - π Middle: Prefer
TryAddEnumerable
For additive pipelines, understand that the last registration typically winsβthese helpers make intent explicit. - π Senior: In shared libraries, always use
TryAdd*
. In apps,Replace
/RemoveAll
enable deterministic composition, but document and test overrides to prevent subtle behavior changes.
β How does the Options pattern work (IOptions
, IOptionsSnapshot
, IOptionsMonitor
), and when to use each?
The Options pattern binds configuration to typed POCOs and injects them via DI.
IOptions<T>
: a singleton snapshot created once (per app lifetime). No reloads.IOptionsSnapshot<T>
: scoped (new value per request/DI scope). Picks up file/config reloads.IOptionsMonitor<T>
: singleton with change notifications (OnChange
) and always returns the latest value.
Example:
// Program.cs β bind config and enable reloads from appsettings.json
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Services.AddOptions<MyFeatureOptions>()
.Bind(builder.Configuration.GetSection("MyFeature"))
.ValidateDataAnnotations()
.Validate(o => o.RetryCount >= 0, "RetryCount must be >= 0");
var app = builder.Build();
app.MapGet("/show",
(Microsoft.Extensions.Options.IOptions<MyFeatureOptions> opts) => opts.Value); // singleton snapshot
app.Run();
public sealed class MyFeatureOptions
{
public string Endpoint { get; set; } = "";
public int RetryCount { get; set; } = 3;
}
// IOptionsSnapshot<T> β request-scoped, updates when config file changes
app.MapGet("/snapshot", (Microsoft.Extensions.Options.IOptionsSnapshot<MyFeatureOptions> snap) =>
{
var o = snap.Value; // recomputed per request (scope)
return Results.Ok(new { o.Endpoint, o.RetryCount });
});
// IOptionsMonitor<T> β singleton with change callback (great for background/singleton services)
public sealed class Worker : BackgroundService
{
private MyFeatureOptions _current;
public Worker(Microsoft.Extensions.Options.IOptionsMonitor<MyFeatureOptions> monitor)
{
_current = monitor.CurrentValue;
monitor.OnChange(newVal => _current = newVal); // hot-reload
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// always uses the latest options
await Task.Delay(TimeSpan.FromSeconds(5), ct);
}
}
}
What .NET engineers should know:
- πΌ Junior: Use
IOptions<T>
for simple, static config;IOptionsSnapshot<T>
updates per request;IOptionsMonitor<T>
updates globally and can notify on changes. - π Middle: Donβt inject
IOptionsSnapshot<T>
into singletons (itβs scoped). For background/singleton services, preferIOptionsMonitor<T>
. EnablereloadOnChange
for live updates. Add validation (Validate
,ValidateDataAnnotations
). - π Senior: Centralize options binding/validation, fail fast on invalid config, and use
OnChange
carefully (debounce/guard) to avoid race conditions. For multi-tenant or named configs, useIOptionsMonitor<T>.Get("name")
andConfigureNamedOptions
.
π Resources: Options pattern in .NET
β What configuration providers are available (Azure Key Vault, AWS Secrets Manager, database)?
ASP.NET Core configuration is provider-based β each provider reads key/value pairs from a source and merges them into a single configuration tree. Beyond built-ins (JSON, INI, environment variables, command-line args), you can plug in cloud secret stores or databases.
Common providers:
Provider | Purpose | When to use |
---|---|---|
JSON / INI / XML files | Appsettings, environment overrides | Default app config |
Environment variables | Deployment-specific settings, container/k8s envs | Cloud-native deployments |
Command-line args | CI/CD overrides, dev/test flags | Quick switches |
User secrets (dotnet user-secrets ) | Local dev secrets | Never commit secrets to source |
Azure Key Vault (Azure.Extensions.AspNetCore.Configuration.Secrets ) | Secure cloud secret store | Azure-hosted apps |
AWS Secrets Manager (Amazon.Extensions.Configuration.SecretsManager ) | Secure cloud secret store | AWS-hosted apps |
AWS Parameter Store (Amazon.Extensions.Configuration.SystemsManager ) | Config/secret store in AWS SSM | AWS apps with frequent updates |
Database (custom provider) | Centralized, DB-backed config | Multi-tenant/shared config |
Redis or Consul (community packages) | Distributed config in memory or service registry | Real-time config updates |
In-memory (MemoryConfigurationProvider ) | Testing, overrides | Unit/integration tests |
Examples:
// Azure Key Vault
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{vaultName}.vault.azure.net/"),
new DefaultAzureCredential());
// AWS Secrets Manager
builder.Configuration.AddSecretsManager(options =>
{
options.SecretsManagerConfig.RegionEndpoint = RegionEndpoint.USEast1;
});
// Database-backed (custom)
builder.Configuration.Add(new DbConfigurationSource(connectionString));
What .NET engineers should know:
- πΌ Junior: Config can come from multiple sources; later providers override earlier ones.
- π Middle: Use cloud secret stores for sensitive data; prefer environment-specific overrides over hardcoding.
- π Senior: Design provider ordering carefully, enable reloads where supported, and centralize config for distributed apps. Secure all secret sources with the least privilege.
π Resources:
- Configuration in ASP.NET Core
- Azure Key Vault configuration provider in ASP.NET Core
- Using AWSSDK.Extensions.NETCore.Setup and the IConfiguration interface
β What pitfalls exist when using scoped services in async background tasks?
Background workers (BackgroundService
/ IHostedService
) are singletons. Injecting scoped services (e.g., EF Core DbContext
) directly into them creates lifetime mismatches and disposal bugs. The safe pattern is: create a new scope per unit of work, resolve scoped services inside that scope, and dispose of the scope when done. Also, avoid longβlived scopes, scope reuse across iterations, and any reliance on HttpContext
(not available off the request pipeline).
βοΈ Correct: create a scope per iteration or batch
public sealed class Worker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<Worker> _log;
public Worker(IServiceScopeFactory scopeFactory, ILogger<Worker> log)
{ _scopeFactory = scopeFactory; _log = log; }
protected override async Task ExecuteAsync(CancellationToken ct)
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
try
{
while (await timer.WaitForNextTickAsync(ct))
{
using var scope = _scopeFactory.CreateScope(); // or: await using var scope = _scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await DoWorkAsync(db, ct);
}
}
catch (OperationCanceledException) { } // graceful shutdown
catch (Exception ex) { _log.LogError(ex, "Background failure"); }
}
}
β Anti-pattern: captive dependency
public sealed class BadWorker : BackgroundService
{
private readonly AppDbContext _db; // β scoped in a singleton
public BadWorker(AppDbContext db) => _db = db;
// Will fail or behave incorrectly; DbContext may outlive its scope and is not thread-safe.
}
Common pitfalls
- Captive dependencies (singleton β scoped)
π οΈ Fix: injectIServiceScopeFactory
; resolve scoped services in a short-lived scope. - Reusing the same scope across loop iterations
π οΈ Fix: new scope per iteration/batch; dispose after use to free pooled connections and DI objects. - Long-running work inside a single scope
π οΈ Fix: keep scopes small; for streams/queues, chunk work into batches, each with its scope. - Parallel processing with shared-scoped instances
π οΈ Fix: never share a scoped service across threads; create independent scopes per parallel worker. - Not honoring
CancellationToken
π οΈ Fix: pass the token to I/O (DbContext
, HTTP) and timers; exit promptly on shutdown to avoid βzombieβ scopes. - Async background tasks using
IOptionsSnapshot<T>
π οΈ Fix: itβs scoped; injectIOptionsMonitor<T>
instead (singleton-friendly, change notifications). - Assuming
HttpContext
/ requesting services exist.
π οΈ Fix: background code runs outside the request pipeline; resolve what you need from your scope. - Sync-over-async /
async void
work
π οΈ Fix: keep everythingasync Task
; wrap with retries/backoff; surface errors to logs/metrics. - Leaking EF Core contexts / thread-safety issues
π οΈ Fix: oneDbContext
per scope; no cross-thread use; dispose with the scope; preferAsNoTracking()
for read paths. - Hidden transitive captive dependencies
π οΈ Fix: review registrations and lifetimes; consider unit tests that resolve the container to catch lifetime mismatches early.
What .NET engineers should know:
- πΌ Junior: Background workers are singletons. Donβt inject
DbContext
directly; make a scope per job and resolve it inside. - π Middle: Avoid long-lived or shared scopes; pass cancellation tokens; use
IOptionsMonitor<T>
(notIOptionsSnapshot<T>
) in background code. - π Senior: Batch work with separate scopes, cap concurrency (e.g.,
SemaphoreSlim
), ensure idempotency, add observability, and validate DI lifetimes in tests/CI.
π Resources:
- Options pattern in .NET
- DbContext Lifetime, Configuration, and Initialization
- Background tasks with hosted services in ASP.NET Core
Web APIs, Controllers & UI Frameworks
This section covers controllers, model binding, validation, and how to choose between API styles.

β What are Minimal APIs in ASP.NET Core, and how do they differ from using Controllers (MVC)?
Minimal APIs are a lightweight way to build HTTP endpoints in ASP.NET Core without the whole MVC framework.
They were introduced in .NET 6 to make small services, microservices, and quick prototypes faster to create.
When to use
- Minimal APIs: Small services, serverless apps, quick prototypes, simple CRUD without heavy validation.
- Controllers: Enterprise apps, complex routing, heavy model binding, filters, versioning.
Minimal API example
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello World!");
app.MapPost("/users", (User user) => $"User {user.Name} created");
app.Run();
Controller (MVC) example
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id) => Ok($"User #{id}");
}
Program.cs
builder.Services.AddControllers();
app.MapControllers();
Key differences between the minimal API and the Controller:
Feature | Minimal APIs | Controllers (MVC) |
---|---|---|
Setup | Very little boilerplate | Requires AddControllers() and attributes |
Structure | Inline route definitions | Separate controller classes & attributes |
Best for | Small APIs, microservices, PoCs | Large, structured applications |
Filters/Model binding | Limited, simpler | Rich filtering, binding, validation |
Testability | Functional, but less structured | More test-friendly with separation |
Performance | Slightly faster for small APIs | Slight overhead from MVC pipeline |
What .NET engineers should know:
- πΌ Junior: Should know that Minimal APIs are just a lighter way to define endpoints compared to controllers.
- π Middle: Should choose Minimal APIs for small services and understand trade-offs (no full MVC features). Should be able to migrate Minimal API endpoints to Controllers if the app grows.
- π Senior: Should the architects choose the right approach per module, mix Minimal APIs and Controllers if needed, and manage concerns like validation, auth, and versioning without MVC overhead
π Resources: Minimal APIs quick reference
β What is model binding and model validation in ASP.NET Core MVC?
Model binding is the process where ASP.NET Core automatically takes incoming HTTP request data β from route values, query strings, form fields, headers, or JSON body β and maps it to method parameters or model objects in your controller or minimal API.
It saves you from manually parsing HttpRequest
and converting strings to C# types.

Example:
public class UserDto
{
public string Name { get; set; }
public int Age { get; set; }
}
[HttpPost("users")]
public IActionResult CreateUser(UserDto user)
{
// "user" is populated automatically from JSON body
return Ok(user);
}
If the request body is:
{ "name": "Alice", "age": 25 }
ASP.NET Core binds it to UserDto automatically.
Binding sources:
- [FromRoute] β URL segments (
/users/5
) - [FromQuery] β Query string (
?page=2
) - [FromBody] β JSON/XML body
- [FromForm] β Form fields
- [FromHeader] β Request headers
- [FromServices] β Injected services
If no attribute is specified:
- For simple types (string, int, etc.), ASP.NET Core binds from the route or query string.
- For complex types, it binds from the request body (in
[ApiController]
-decorated controllers).
What .NET engineers should know:
- πΌ Junior: Should know model binding automatically converts HTTP request data into C# parameters or models.
- π Middle: Should use binding attributes to control data sources and handle binding for complex objects.
- π Senior: Should customize model binding for advanced scenarios (custom binders, performance tuning) and design APIs with clear binding rules.
π Resources: Model Binding in ASP.NET Core
β What is model validation in ASP.NET Core MVC?
Model Validation occurs after binding. If you have validation attributes on your model properties (like [Required]
, [Range]
, [EmailAddress]
, etc.), the framework will automatically validate the model and record any errors. In ASP.NET Core, if you have the [ApiController]
attribute on a controller, it even automatically returns a 400 Bad Request with validation errors if the model state is invalid (this saves you from manually checking ModelState.IsValid
in API scenarios). In Razor Pages or MVC views, you often check ModelState
or use tag helpers that display validation messages.

So in practice, When a request comes in:
- Routing picks the action and provides some route values.
- The model binder binds simple action parameters (e.g.
int id
from route or query) and complex types (like aProductDto
from the JSON body or posted form). - The bound object is validated. You can also trigger manual validation (e.g.
TryValidateModel
) if needed. - If errors exist, they are stored in
ModelState
. An ApiController will short-circuit to 400 with those errors in the response by default. In MVC views, you might re-display the form with error messages.
This system greatly simplifies input handling by abstracting the conversion of HTTP data to .NET objects and checking constraints, so you can assume in your action that the input model is already parsed and (hopefully) valid.
Example:
public class UserDto
{
[Required]
[StringLength(50)]
public string Name { get; set; }
[Range(18, 99)]
public int Age { get; set; }
}
[HttpPost("users")]
public IActionResult CreateUser(UserDto user)
{
if (!ModelState.IsValid) // In [ApiController], this check is automatic
return BadRequest(ModelState);
return Ok(user);
}
Request:
{ "age": 15 }
Response:
{
"errors": {
"Name": ["The Name field is required."],
"Age": ["The field Age must be between 18 and 99."]
}
}
Common validation sources
- Data Annotations:
[Required]
,[Range]
,[EmailAddress]
, etc. - Custom Validation Attributes: Your own rules via
ValidationAttribute
. - FluentValidation: A Popular external library for complex validation.
What .NET engineers should know about validations:
- πΌ Junior: Should know how to apply Data Annotations and check
ModelState.IsValid
. - π Middle: Should use
[ApiController]
for automatic validation, create custom validation attributes, and return meaningful error responses. - π Senior: Should design complex validation workflows (e.g., conditional rules, async checks), integrate external validators like FluentValidation, and ensure performance for large models.
π Resources: Model validation in ASP.NET Core MVC and Razor Pages
β What are filters in ASP.NET Core, and how do they differ from middleware?
Filters are part of the MVC framework (controllers and Razor Pages) that let you run code at specific points during action execution β before an action runs, after it runs, when results are being processed, or when exceptions occur. Types include Authorization filters, Resource filters, Action filters, Exception filters, and Result filters.
Middleware, by contrast, runs earlier in the request pipeline β before MVC routing β and operates globally for all requests, regardless of whether they hit MVC.
Endpoint filters (introduced in .NET 7) sit between middleware and MVC filters. They run after routing but before the selected endpoint executes, and work with both minimal APIs and MVC endpoints. This makes them great for cross-cutting concerns that are endpoint-specific but not tied to MVCβs full filter pipeline.

Hereβs a breakdown of how they differ:
Aspect | Middleware | Endpoint Filters (.NET 7+) | MVC Filters |
---|---|---|---|
Scope | Global, all requests | Per endpoint (minimal API or MVC) | Controller/action level |
Runs | Before routing | After routing, before endpoint execution | After endpoint selection, inside MVC |
Use cases | Auth, CORS, logging, headers | Per-endpoint validation, logging, shaping | Model validation, authorization, result transformation |
Configured via | app.Use...() | app.MapGet(...).AddEndpointFilter(...) | Attributes or options.Filters.Add() |
Example of a simple action filter that logs before and after an action runs:
public class LogActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
=> Console.WriteLine("Before action executes.");
public void OnActionExecuted(ActionExecutedContext context)
=> Console.WriteLine("After action executes.");
}
You can register it globally in Startup.cs
:
services.AddControllers(o => o.Filters.Add<LogActionFilter>());
Or apply it to a specific controller/action:
[ServiceFilter(typeof(LogActionFilter))]
public class MyController : Controller { }
Example β simple Endpoint Filter (.NET 7+ minimal API):
app.MapGet("/hello", () => "Hi")
.AddEndpointFilter(async (context, next) =>
{
Console.WriteLine("Before endpoint");
var result = await next(context);
Console.WriteLine("After endpoint");
return result;
});
What .NET engineers should know:
- πΌ Junior: Should know that middleware is global and runs early, while filters apply to controllers and actions. Basic understanding of
UseMiddleware
vs[Authorize]
,[ValidateModel]
, etc. - π Middle: Expected to create custom filters for logging, validation, and error handling. Understand the lifecycle differences and when to choose one over the other.
- π Senior: Should design reusable, DI-enabled filters. Know how to balance concerns between middleware and filters for maintainability. Understand execution order and early termination logic.
π Resources: Filters in ASP.NET Core
β What is the difference between Razor Pages and MVC in ASP.NET Core, and when might you use Razor Pages?
Razor Pages is page-focused β each page has its UI (.cshtml) and logic (PageModel) together. MVC splits logic into controllers and views, offering more flexibility but more boilerplate. Razor Pages is excellent for simple, page-centric apps; MVC suits complex apps with shared logic.
Example:
// Razor Page
public class IndexModel : PageModel
{
public string Message { get; set; } = "Hello!";
}
@page
<h1>@Model.Message</h1>
What .NET engineers should know:
- πΌ Junior: Razor Pages keeps page logic and view together, MVC separates them.
- π Middle & π Senior: Understand routing, handlers, and when MVC is better for shared logic.
π Resources:
β What is Blazor, and how do Blazor WebAssembly and Blazor Server differ?
Blazor is a framework for building interactive web UIs using C# instead of JavaScript. It runs .NET code directly in the browser (WebAssembly) or on the server (SignalR).
Blazor WebAssembly runs the app in the browser, downloading the .NET runtime and app files. It works offline but has a larger initial load. Blazor Server runs on the server, sending UI updates over a real-time connection β lighter to load but needs constant connectivity.
What .NET engineers should know:
- πΌ Junior: Should know that Blazor lets you write C# for the browser. WebAssembly runs client-side; the Server runs on the server.
- π Middle: Know trade-offs between WebAssembly and Blazor Server
- π Senior: Architect Blazor solutions with hosting model choice based on performance, scalability, and deployment constraints; mix hosting models if required.
π Resources:
β How to implement API versioning in ASP.NET Core (URL, query, header)?
API versioning in ASP.NET Core is handled via the Microsoft.AspNetCore.Mvc.Versioning package.
You can expose different versions through:
- URL path β
/api/v1/products
- Query string β
/api/products?api-version=1.0
- Header β using a custom header like
x-api-version: 1.0
Example using URL versioning:
builder.Services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
o.ReportApiVersions = true;
o.ApiVersionReader = new UrlSegmentApiVersionReader();
});
Controller:
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
public class ProductsV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok("Version 1");
}
What .NET engineers should know:
- πΌ Junior: Know that API versioning allows you to change APIs without breaking old clients.
- π Middle: Understand how to configure versioning via URL, query, and headers, and when to use each.
- π Senior: Design a long-term versioning strategy, manage multiple versions in production, and ensure backward compatibility.
π Resources: aspnet-api-versioning
β How to handle file uploads and large requests in ASP.NET Core?
ASP.NET Core supports file uploads via IFormFile
(in-memory) or streaming to disk/other storage to avoid memory spikes. For large requests, increase MaxRequestBodySize
and use streaming to prevent loading the whole file into memory.
Example (streaming to disk):
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
var path = Path.Combine("uploads", file.FileName);
using var stream = new FileStream(path, FileMode.Create);
await file.CopyToAsync(stream);
return Ok("Uploaded");
}
Startup adjustments:
app.Use(async (context, next) =>
{
context.Features.Get<IHttpMaxRequestBodySizeFeature>().MaxRequestBodySize = 1073741824; // 1 GB
await next();
});
You can also configure Kestrelβs limits or use [RequestSizeLimit]
attribute on specific controllers or actions.
What .NET engineers should know:
- πΌ Junior: Know IFormFile exists and can be used to upload files.
- π Middle: Understand the difference between in-memory and streamed uploads, and how to change request size limits.
- π Senior: Implement secure large-file handling with streaming, virus scanning, resumable uploads, and cloud storage integration.
π Resources: Upload large files with streaming
β How does content negotiation work, and how do you customize response formatting?
Content negotiation in ASP.NET Core picks the best response format (JSON, XML, plain text) based on the requestβs Accept
header and available formatters. By default, it uses JSON with System.Text.Json
. You can add or customize formatters (e.g., XML) in AddControllers()
.
Example (adding XML formatter):
builder.Services.AddControllers()
.AddXmlSerializerFormatters();
Example (forcing a specific format in action):
[HttpGet("{id}")]
[Produces("application/xml")]
public ActionResult<Product> Get(int id) => new Product { Id = id, Name = "Book" };
What .NET engineers should know:
- πΌ Junior: Know that ASP.NET Core chooses the output format based on the
Accept
header. - π Middle: Understand how to add/remove formatters and override content negotiation in controllers or actions.
- π Senior: Implement custom formatters, negotiate based on custom logic, and ensure consistent formatting across APIs.
π Resources:
β What is gRPC in ASP.NET Core, and when should you use it over REST?
gRPC is a high-performance, contract-based RPC framework using HTTP/2 and Protocol Buffers. Itβs great for internal service-to-service calls because itβs faster, strongly typed, and supports streaming.
Compared to REST, itβs less human-readable but more efficient over the wire.
Example service definition (.proto
):
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }
What .NET engineers should know:
- πΌ Junior: Know gRPC is an alternative to REST for calling services, using
.proto
contracts. - π Middle: Understand gRPC advantages like speed, streaming, and strong typing, and that it requires HTTP/2.
- π Senior: Decide when to use gRPC (e.g., internal microservices) vs REST, design efficient contracts, and handle backward compatibility in
.proto
files.
π Resources: Overview for gRPC on .NET
Security and Identity
Security is built into ASP.NET Core with features like authentication, authorization, and identity management. This section explains how to protect your applications using modern security practices.

β What are common approaches to authentication in ASP.NET Core (cookie-based auth vs JWT bearer tokens)?
ASP.NET Core supports several authentication approaches, each fitting different app types:
- Cookie-based authentication β Server issues a cookie after login; used for MVC/Razor Pages. Stores user session state on the server or in an encrypted cookie.
- JWT bearer tokens β Client gets a signed token (often via OAuth 2.0 / OpenID Connect) and sends it in the
Authorization
header; ideal for APIs and SPAs. - OpenID Connect / OAuth 2.0 β Delegates authentication to identity providers (Azure AD, Auth0, Okta, IdentityServer). Usually returns cookies (web apps) or JWT (APIs).
- Windows Authentication β Uses existing Active Directory/Kerberos/NTLM credentials; common in intranet apps.
- Custom schemes β For integrating legacy or external auth systems (e.g., API keys, HMAC signatures).
Approach | Best for | Where identity lives | Stateful? | Pros | Cons | Typical setup |
---|---|---|---|---|---|---|
Cookie-based auth | MVC/Razor Pages web apps | Server issues an auth cookie (ticket) to the browser | Stateful (or ticket-style) | Simple sign-in flows, built-in anti-CSRF, works great with server-rendered UI | Harder for APIs/mobile; session storage/affinity or ticket mgmt | AddAuthentication().AddCookie() |
JWT Bearer tokens | Public APIs, SPAs, mobile | Token carried by client in Authorization: Bearer | Stateless | Scales horizontally, no server session store, easy cross-client | Larger tokens, rotation/expiration needed, careful client storage | AddAuthentication().AddJwtBearer() |
OpenID Connect (OIDC) | Enterprise SSO, external IdPs | IdP issues cookie (web) or JWT (API) | Depends on flow | Delegated auth, SSO, policy/compliance offloaded | More moving parts, needs IdP availability | AddOpenIdConnect() / OIDC with cookie or JWT |
OAuth 2.0 (Auth for APIs) | Third-party API access, SPAs/mobile | Access/refresh tokens | Stateless | Broad ecosystem, scopes/consent | Refresh flow complexity, token revocation strategy | AddJwtBearer() + OAuth flows |
Windows Authentication | Intranets on Windows/AD | Kerberos/NTLM via browser | Stateless per request | Zero passwords, seamless SSO in domain | Limited to Windows/AD environments | UseAuthentication() with Windows auth |
API key / HMAC (custom) | Service-to-service calls | Header or signature per request | Stateless | Simple, fast, no cookie overhead | Key distribution/rotation, coarse identity | Custom AuthenticationHandler |
What .NET engineers should know about authentication in ASP.NET Core:
- πΌ Junior: Cookies are best for server-rendered sites, JWT for APIs. Know that OAuth/OpenID lets you log in with external providers.
- π Middle: Understand token lifetimes, refresh tokens, CSRF vs XSS risks, and when to prefer stateful vs stateless auth.
- π Senior: Design auth flows with security, scalability, and compliance in mind. Choose schemes per boundary, implement token rotation, secure storage, and proper validation.
π Resources:
- Overview of ASP.NET Core authentication
- Cookie authentication
- JWT bearer authentication
- OpenID Connect in ASP.NET Core
β What is policy-based authorization in ASP.NET Core, and how is it different from role-based authorization?
Role-based authorization checks if a user belongs to a specific role (e.g., Admin
). Policy-based authorization lets you define richer rules (policies) that can check claims, values, or custom logic β roles can be part of a policy, but policies arenβt limited to roles.
Example of policy-based auth:
// Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Over18", policy =>
policy.RequireClaim("Age", "18", "19", "20"));
});
// Controller
[Authorize(Policy = "Over18")]
public IActionResult AdultsOnly() => View();
What .NET engineers should know:
- πΌ Junior: Roles are simple βlabelsβ for users; policies can use roles but can also check claims or custom conditions.
- π Middle: Know how to define and register policies, combine multiple requirements, and use custom
IAuthorizationHandler
for complex checks. - π Senior: Design a consistent authorization model mixing policies and roles, centralize security rules, and make them testable and maintainable.
β What is CORS, and how to configure CORS in an ASP.NET Core application?
CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks requests to your API from different origins unless explicitly allowed. Itβs essential when your frontend and backend run on other domains, ports, or protocols.
Common CORS misconfigurations (and fixes)
AllowAnyOrigin()
+AllowCredentials()
Browsers disallow this combination; itβs insecure.
π οΈ Fix: useWithOrigins("https://app.example.com") .AllowCredentials()
or keepAllowAnyOrigin()
without credentials.- Wrong middleware order/preflight not handled
IfUseCors()
runs too late, preflight (OPTIONS
) fails.
π οΈ Fix: place it after routing and before auth/authorization: - Origin strings donβt match exactly (scheme/host/port)
http://localhost:3000
βhttps://localhost:3000
.
π οΈ Fix: list precise origins (no trailing slash), e.g..WithOrigins("https://myapp.com","http://localhost:5173")
. - Using wildcards when sending cookies/SignalR with credentials
Credentials require a specific origin andAllowCredentials()
.
π οΈ Fix: enumerate trusted origins; avoid*
. - Forgetting custom headers/methods or exposed headers
Preflight fails if you send non-simple headers (e.g.,Authorization
) or need to read custom response headers.
π οΈ Fix:.AllowHeaders("Authorization","Content-Type") .AllowMethods("GET","POST") .WithExposedHeaders("Location","X-Total-Count")
. - Overly permissive defaults in production
AllowAnyHeader().AllowAnyMethod()
everywhere widen your blast radius.
π οΈ Fix: narrow per environment or endpoint (named policies,[EnableCors]
). - Subdomain/tenant patterns not covered
You need multiple origins (e.g.,https://*.example.com
)βWithOrigins
doesnβt support wildcards.
π οΈ Fix: useSetIsOriginAllowed(origin => /* validate pattern or registry */)
with care. - Trailing slash in origins
WithOrigins("https://myapp.com/")
wonβt match.
π οΈ Fix: remove the slash. - Missing cache for preflight
High traffic gets hammered byOPTIONS
.
π οΈ Fix:.SetPreflightMaxAge(TimeSpan.FromHours(1))
(tune to your risk posture). - Mixing global and attribute CORS in conflicting ways
Route-level[DisableCors]
Or differently named policies can surprise you.
π οΈ Fix: prefer a single global policy, add exceptions explicitly.
A safe baseline (production-minded)
builder.Services.AddCors(opt =>
{
opt.AddPolicy("Frontend", p => p
.WithOrigins("https://app.example.com","https://admin.example.com","http://localhost:5173")
.WithMethods("GET","POST","PUT","DELETE")
.WithHeaders("Authorization","Content-Type")
.WithExposedHeaders("Location","X-Total-Count")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(30)));
});
What .NET engineers should know:
- πΌ Junior: Knows that CORS is a browser check; origins must match exactly; donβt mix
*
with credentials. - π Middle: Aware that order matters; able to configure headers/methods/exposed headers; per-env policies; preflight caching.
- π Senior: Treat CORS as part of your threat model; validate origins programmatically for multi-tenant; monitor preflight rates; least-privilege configs by endpoint.
π Resources:
β What is ASP.NET Core Identity, and when should you use it?
ASP.NET Core Identity is a membership system for handling authentication, authorization, and user management (registration, login, roles, claims). Itβs useful when you need built-in, customizable auth with minimal setup, without building everything from scratch.
Example (setup in Program.cs
):
builder.Services.AddDbContext<AppDbContext>(...);
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
What .NET engineers should know:
- πΌ Junior: Know Identity provides ready-to-use authentication and user management features.
- π Middle: Understand how to customize Identity (tables, password rules, roles) and integrate with external logins like Google or Microsoft.
- π Senior: Decide when to use Identity vs custom auth, secure it with best practices, and integrate it into microservices or external identity providers.
π Resources:
β What is the Data Protection API, and how does ASP.NET Core handle key management?
The Data Protection API in ASP.NET Core provides cryptographic services for encrypting and decrypting data (e.g., auth cookies, CSRF tokens). It uses a key ring to store and rotate encryption keys automatically. By default, keys are stored locally on disk, but you can persist them in Redis, Azure Key Vault, or a database for multi-instance apps.
Example (store keys in Redis):
builder.Services.AddDataProtection()
.PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect("localhost"), "DataProtection-Keys");
What .NET engineers should know:
- πΌ Junior: Know it encrypts sensitive data like cookies in ASP.NET Core.
- π Middle: Understand key lifetime, storage options, and why shared storage is needed in load-balanced environments.
- π Senior: Design secure key storage strategies, integrate with cloud key vaults, and manage key rotation policies.
π Resources:
β How to implement CSRF protection in ASP.NET Core?
ASP.NET Core automatically adds CSRF (anti-forgery) protection to Razor Pages and MVC by validating an anti-forgery token sent with each form POST. You generate the token with @Html.AntiForgeryToken()
and validate it with [ValidateAntiForgeryToken]
on actions. For APIs, use the IAntiforgery
service if needed.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(DataModel model) => Ok();
What .NET engineers should know:
- πΌ Junior: Know that CSRF is an attack where malicious sites trick users into making unwanted requests.
- π Middle: Understand how anti-forgery tokens work and when you need to apply them (non-GET requests from browsers).
- π Senior: Implement CSRF protection for complex cases (SPAs, APIs), combine with same-site cookies, and balance security with usability.
π Resources: Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core
β What is the SameSite cookie setting, and why does it matter for cross-domain scenarios?
The SameSite
cookie setting controls whether a browser sends cookies with cross-site requests.
Strict
only sent for same-site navigationLax
sent for top-level GET requests (default in many cases)None
sent for all requests, but requiresSecure
flag
It matters because browsers block cross-site cookies by default, affecting scenarios like third-party logins, embedded iframes, or APIs on a different domain.
Example:
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.None,
Secure = CookieSecurePolicy.Always
});
What .NET engineers should know:
- πΌ Junior: Know if cookies are sent in cross-site requests.
- π Middle: Understand the difference between Strict, Lax, and None, and their security trade-offs.
- π Senior: Configure cookies correctly for SSO, third-party integrations, and ensure compliance with modern browser rules.
π Resources: Work with SameSite cookies in ASP.NET Core
β What OWASP-recommended security features should be enabled in ASP.NET Core?
ASP.NET Core includes many built-in features that align with OWASP Top 10 recommendations:
- HTTPS enforcement β
app.UseHttpsRedirection()
- HSTS β
app.UseHsts()
to force HTTPS for future requests - Secure cookies β set
Secure
,HttpOnly
, andSameSite
- Data Protection API β for encrypting sensitive data
- Anti-forgery tokens β to prevent CSRF
- Input validation & model binding β prevents injection attacks
- Authentication & Authorization middleware β role/claims-based
- Rate limiting & throttling β to prevent abuse (e.g.,
AspNetCoreRateLimit
package)
What .NET engineers should know:
- πΌ Junior: Know the framework has built-in security middleware like HTTPS redirection, cookies, and CSRF protection.
- π Middle: Understand how to configure these features and why each is needed.
- π Senior: Map OWASP Top 10 risks to ASP.NET Core protections, customize security policies and integrate security scanning into CI/CD.
π Resources:
Performance, Monitoring & Background Processing
This section explores optimization techniques, monitoring tools, and strategies for running background tasks.

β What logging capabilities does ASP.NET Core provide, and how do you configure logging?
ASP.NET Core ships a unified logging abstraction (ILogger) with structured logging, scopes, filtering, and pluggable providers (Console, Debug, EventSource/EventLog, Azure App Insights, Serilog, etc.). You configure it via code and/or appsettings.json; for hot paths, use LoggerMessage/source-generated logging for fewer allocations and better throughput.
Program.cs
// Program.cs (minimal, production-friendly defaults)
var builder = WebApplication.CreateBuilder(args);
// 1) Configure providers &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp; filters
builder.Logging.ClearProviders(); // start clean, then add what you need
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// builder.Logging.AddEventSourceLogger(); // optional
// builder.Logging.AddEventLog(); // Windows only
builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); // reduce noise
builder.Logging.AddFilter("MyApp", LogLevel.Information);
// 2) Read levels from appsettings.json (logging section shown below is applied by default)
builder.Host.ConfigureLogging(logging => logging.AddConfiguration(builder.Configuration.GetSection("Logging")));
var app = builder.Build();
// 3) Use logging with scopes and structured data
app.MapGet("/health", (ILoggerFactory lf) =>
{
var logger = lf.CreateLogger("MyApp.Health");
using (logger.BeginScope(new Dictionary<string, object?> { ["correlationId"] = Guid.NewGuid() }))
{
logger.LogInformation("Health check OK at {utc}", DateTime.UtcNow);
}
return Results.Ok(new { status = "ok" });
});
app.Run();
appsettings.json
// appsettings.json (typical logging section)
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
What .NET engineers should know:
- πΌ Junior: Use ILogger for structured logs; pick providers (Console, Debug, etc.); set levels in appsettings.json.
- π Middle: Filter by category, use scopes for correlation IDs, and tune noise from framework categories (e.g., Microsoft.AspNetCore Warning).
- π Senior: For hot paths, prefer LoggerMessage/source-generated logs; centralize sinks (e.g., App Insights/Serilog), define retention/PII policy, and standardize log schema across services.
π Resources:
- Logging in .NET and ASP.NET Core
- Logging in C# and .NET
- Logging providers in .NET
- High-performance logging in .NET
- Compile-time logging source generation
β How do you implement caching in ASP.NET Core, and what are the different types of caching available (in-memory, distributed, response/output caching)?
Caching is a key performance feature, and ASP.NET Core supports several kinds:
- In-Memory Caching: This is caching data in the memory of the web server (for a single node).
// In-memory caching (fast, per-process)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
var app = builder.Build();
app.MapGet("/weather", async (IMemoryCache cache) =>
{
return await cache.GetOrCreateAsync("wx:today", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
entry.SlidingExpiration = TimeSpan.FromMinutes(2);
return Task.FromResult(new { temp = 23 });
});
});
app.Run();
- Distributed Caching: This is for scenarios where you have multiple servers or want the cache persisted externally (e.g., Redis or SQL Server).
// Distributed caching (shared across instances; e.g., Redis)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
var app = builder.Build();
app.MapGet("/profile/{id}", async (string id, IDistributedCache cache) =>
{
var key = $"profile:{id}";
var json = await cache.GetStringAsync(key);
if (json is null)
{
var profile = new { id, name = "Ada" }; // load from DB in real life
json = System.Text.Json.JsonSerializer.Serialize(profile);
await cache.SetStringAsync(key, json, new DistributedCacheEntryOptions
{ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15) });
}
return Results.Text(json, "application/json");
});
app.Run();
- Response Caching (HTTP caching): This is about caching at the HTTP response level, mostly to leverage browser/proxy caches.
// Response Caching (HTTP 1.1 semantics; respects Cache-Control)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
app.MapGet("/news", (HttpContext ctx) =>
{
ctx.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{ Public = true, MaxAge = TimeSpan.FromSeconds(30) };
return Results.Text("cached for 30s");
});
app.Run();
- Output Caching (new in .NET 7): This is a newer server-side output cache that can cache the actual output of controllers or pages in memory (separately from the above memory cache) and serve it directly on subsequent requests, bypassing executing the controller again. Itβs like the old OutputCache in ASP.NET or like a built-in reverse-proxy cache. The difference from Response Caching is that Output Caching does not rely purely on HTTP cache headers β itβs configured in the app to decide what to cache.
// Output Caching (.NET 7+; smarter server-side response cache with policies)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(); // AddOutputCache(options => options.AddPolicy("anon", p => p.Expire(TimeSpan.FromMinutes(1))))
var app = builder.Build();
app.UseOutputCache();
app.MapGet("/products", () => Results.Ok(new[] { "A", "B" })).CacheOutput(p => p
.Expire(TimeSpan.FromSeconds(60))
.SetVaryByQuery("page","size") // different cache entries per query
);
app.Run();
So, in practice:
If you want to cache data (like results of computations or DB calls), use MemoryCache or DistributedCache depending on your deployment. If you're going to cache entire responses for performance, use ResponseCache headers for client-side/proxy caching and/or the Output Caching middleware for server-side. They can be complementary: e.g., output cache your pages on the server for 30 sec, also allow the client to cache for that duration.
What .NET engineers should know about cache:
- πΌ Junior: Understand that in-memory is fastest but per-instance; distributed works across servers; response/output caching avoids recomputing complete responses.
- π Middle: Pick keys carefully, set absolute/sliding expirations, and invalidate on data changes. Use distributed cache for multi-node apps; prefer OutputCache over ResponseCaching for richer policies.
- π Senior: Establish cache hierarchy (output > distributed > in-memory), define eviction/TTL strategy per data class, add cache-busting on writes, measure hit ratio, and protect against stampedes (locks, pre-warm, background refresh).
π Resources:
- In-memory caching
- Distributed caching
- Response caching middleware
- Output caching (.NET 7+)
- Cache tag helpers (Razor)
β What is the rate-limiting middleware in ASP.NET Core, and why is it useful?
Rate-limiting middleware in ASP.NET Core controls how many requests a client can make in a given time window, protecting APIs from abuse, preventing resource exhaustion, and ensuring fair usage across clients. Introduced in .NET 7, it supports fixed window, sliding window, token bucket, and concurrency limiting. .NET 8 enhanced this with partitioned rate limiting (per-client limits, e.g., by IP or user) and global rate limiting policies for simplified configuration. For multi-instance apps (e.g., in Kubernetes), rate-limiting state can be shared via distributed stores like Redis, ensuring consistency across nodes.
Example:
// Program.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Caching.StackExchangeRedis;
var builder = WebApplication.CreateBuilder(args);
// Add Redis for distributed rate limiting
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
// Configure rate limiting
builder.Services.AddRateLimiter(options =>
{
// Global limiter (applies to all endpoints unless overridden)
options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(resource =>
{
return new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
{
PermitLimit = 100, // Global limit: 100 requests per minute
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0,
AutoReplenishment = true
});
})
);
// Endpoint-specific partitioned limiter (per IP)
options.AddFixedWindowLimiter("fixed-per-ip", new FixedWindowRateLimiterOptions
{
PermitLimit = 5, // 5 requests per 10 seconds per IP
Window = TimeSpan.FromSeconds(10),
QueueLimit = 0,
AutoReplenishment = true
}).WithPartitioner(ctx => ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown");
// Customize 429 response
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
var limiter = context.Lease.GetRateLimitMetadata();
if (limiter?.RetryAfter != null)
{
context.HttpContext.Response.Headers.RetryAfter = ((int)limiter.RetryAfter.Value.TotalSeconds).ToString();
}
await context.HttpContext.Response.WriteAsync(
$"Too many requests. Retry after {limiter?.RetryAfter?.TotalSeconds ?? 10}s", cancellationToken);
};
});
var app = builder.Build();
app.UseRateLimiter();
// Apply rate limiting to endpoint
app.MapGet("/data", () => "OK").RequireRateLimiting("fixed-per-ip");
app.Run();
Common Rate Limiter Types in ASP.NET Core:
Limiter type | How it works | Best for | Pros | Cons |
---|---|---|---|---|
Fixed window | Resets counters at fixed intervals (e.g., 5 requests per 10 seconds). | Simple APIs, predictable bursts. | Easy to reason about, low overhead. | Burst allowed at window edges (boundary problem). |
Sliding window | Tracks requests in rolling intervals, smoothing bursts. | APIs needing more even traffic. | Fairer distribution, avoids edge bursts. | Slightly higher CPU/memory overhead. |
Token bucket | Tokens added at a steady rate; requests consume tokens. | Services needing burst capacity but steady refill. | Flexible; supports bursts with controlled average rate. | More complex tuning. |
Concurrency | Limits number of concurrent executions. | Resource-heavy ops (file processing, DB calls). | Protects from overloading resource pools. | Doesnβt limit total request rate over time. |
Partitioned | Applies limits per key (e.g., IP, user ID). | Multi-user APIs, fairness across clients.Granular control, scalable with distributed stores. | Granular control, scalable with distributed stores. | Requires careful key design, storage setup |
What .NET engineers should know:
- πΌ Junior: Rate limiting controls how many requests a user can send in a period; prevents abuse.
- π Middle: Know limiter types (fixed/sliding window, token bucket, concurrency), set limits per route/policy, and return proper status codes (429).
- π Senior: Design limits per API tier, use partitioned keys (IP, user, API key), monitor and tune thresholds, and integrate with distributed stores for multi-instance apps.
π Resources: Rate limiting middleware in ASP.NET Core
β How would you implement health checks in ASP.NET Core, and what are they used for?
Health checks expose simple endpoints that report app and dependency health. Theyβre used by load balancers and orchestrators (Kubernetes liveness/readiness probes) to route traffic, restart stuck instances, and fail fast when a dependency is down.
// Program.cs β register checks with tags for liveness/readiness
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
.AddSqlServer(builder.Configuration.GetConnectionString("Sql"), tags: new[] { "ready" })
.AddRedis(builder.Configuration.GetConnectionString("Redis"), tags: new[] { "ready" });
var app = builder.Build();
// Map endpoints (separate for Kubernetes probes)
app.MapHealthChecks("/health/live", new HealthCheckOptions {
Predicate = r => r.Tags.Contains("live"),
ResponseWriter = HealthCheckResponseWriters.WriteMinimalJson // see custom writer below
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions {
Predicate = r => r.Tags.Contains("ready"),
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status200OK,
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
},
ResponseWriter = HealthCheckResponseWriters.WriteMinimalJson
});
app.Run();
// Minimal JSON writer (keeps payload small for probes)
static class HealthCheckResponseWriters
{
public static Task WriteMinimalJson(HttpContext ctx, HealthReport r)
{
ctx.Response.ContentType = "application/json";
var summary = new {
status = r.Status.ToString(),
duration = r.TotalDuration.TotalMilliseconds,
entries = r.Entries.ToDictionary(
kv => kv.Key,
kv => new { status = kv.Value.Status.ToString(), duration = kv.Value.Duration.TotalMilliseconds })
};
return ctx.Response.WriteAsJsonAsync(summary);
}
}
Custom dependency check:
// Custom dependency check
using Microsoft.Extensions.Diagnostics.HealthChecks;
public sealed class S3PingHealthCheck : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken token = default)
{
try
{
// ping your service/dependency here
await Task.Delay(10, token);
return HealthCheckResult.Healthy();
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("S3 unreachable", ex);
}
}
}
What .NET engineers should know:
- πΌ Junior: Health checks are endpoints for liveness/readiness; add with
AddHealthChecks()
and map withMapHealthChecks
. - π Middle: Tag checks and split endpoints; return 503 for unhealthy; keep payloads tiny; avoid heavy work in checks.
- π Senior: Make checks idempotent and fast (<100β300 ms), degrade instead of fail where possible, guard against cascades, and align probe timings (initialDelay/period/timeout) with real startup/dep latencies.
π Resources:
β Why is using HttpClientFactory
recommended for making HTTP calls in ASP.NET Core?
IHttpClientFactory
manages HttpClient
lifetimes and configuration. Creating HttpClient
with new
can cause socket exhaustion (too many ephemeral TCP ports in TIME_WAIT) or DNS stale entries because connections arenβt appropriately reused. HttpClientFactory
pools HttpMessageHandler
instances, handles DNS refresh, supports named/typed clients, and integrates Polly for retries, timeouts, and circuit breakers.
// Program.cs β register a named and typed client
builder.Services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp");
});
builder.Services.AddHttpClient<WeatherClient>(c =>
{
c.BaseAddress = new Uri("https://api.weather.com/");
});
// Typed client
public class WeatherClient
{
private readonly HttpClient _http;
public WeatherClient(HttpClient http) => _http = http;
public Task<string> GetForecastAsync() => _http.GetStringAsync("/forecast");
}
What .NET engineers should know:
- πΌ Junior:
HttpClientFactory
avoids socket exhaustion and makesHttpClient
reuse safe. - π Middle: Use named clients for per-service configs; typed clients for DI; add Polly handlers for retries/timeouts.
- π Senior: Centralize outbound HTTP configs, track metrics, set handler lifetimes for DNS refresh, and secure outbound calls with auth tokens and resilience policies.
π Resources:
β How can you run background tasks or scheduled jobs in ASP.NET Core?
ASP.NET Core offers two main paths: built-in hosted services for in-process work, and external schedulers for durable, cron-style jobs. Use BackgroundService/IHostedService for lightweight, app-lifecycle tasks; use Quartz.NET or Hangfire (or platform schedulers like Azure Functions Timer/Kubernetes CronJob) for persistent, retryable scheduling.
Periodic background task with BackgroundService + PeriodicTimer (.NET 6+)
public sealed class EmailDigestWorker : BackgroundService
{
private readonly ILogger<EmailDigestWorker> _log;
public EmailDigestWorker(ILogger<EmailDigestWorker> log) => _log = log;
protected override async Task ExecuteAsync(CancellationToken ct)
{
var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
try
{
while (await timer.WaitForNextTickAsync(ct))
{
// do work; honor ct; keep it short &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp; resilient
_log.LogInformation("Sending email digests at {ts}", DateTimeOffset.UtcNow);
}
}
catch (OperationCanceledException) { }
}
}
// Program.cs
builder.Services.AddHostedService<EmailDigestWorker>();
Queued background work (producer/consumer) using Channel<T>
public interface IBackgroundQueue<T> { ValueTask EnqueueAsync(T item, CancellationToken ct); IAsyncEnumerable<T> DequeueAsync(CancellationToken ct); }
public sealed class BackgroundQueue<T> : IBackgroundQueue<T>
{
private readonly Channel<T> _ch = Channel.CreateUnbounded<T>();
public ValueTask EnqueueAsync(T item, CancellationToken ct) => _ch.Writer.WriteAsync(item, ct);
public IAsyncEnumerable<T> DequeueAsync(CancellationToken ct) => _ch.Reader.ReadAllAsync(ct);
}
public sealed class QueueWorker : BackgroundService
{
private readonly IBackgroundQueue<string> _q; private readonly ILogger<QueueWorker> _log;
public QueueWorker(IBackgroundQueue<string> q, ILogger<QueueWorker> log) { _q = q; _log = log; }
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var job in _q.DequeueAsync(ct))
{
_log.LogInformation("Processing job {job}", job);
// processβ¦
}
}
}
// Program.cs
builder.Services.AddSingleton<IBackgroundQueue<string>, BackgroundQueue<string>>();
builder.Services.AddHostedService<QueueWorker>();
What .NET engineers should know:
- πΌ Junior: Use
BackgroundService
for periodic tasks; map intervals withPeriodicTimer
; donβt block threads; honor cancellation tokens. - π Middle: Can add retries/timeouts; isolate I/O errors; persist state if work must survive restarts.
- π Senior: For production-ready scheduling, use Quartz.NET/Hangfire or platform schedulers (Azure Functions Timer, Kubernetes CronJob). Ensure idempotency, observability (metrics/traces), backoff strategies, and graceful shutdown.
π Resources:
- Background tasks with hosted services
- Queued background tasks
- Quartz.NET for ASP.NET Core
- Azure Functions Timer trigger
β What is SignalR in ASP.NET Core, and what are its typical use cases?
SignalR is a real-time communication library that lets server and client push messages instantly without polling. It uses WebSockets when possible, falling back to Server-Sent Events or Long Polling. ASP.NET Core supports typed hubs, automatic reconnect, scaling with backplanes, and streaming.
Typical use cases:
- Live dashboards (metrics, IoT readings, analytics)
- Chat/messaging apps
- Notifications (e.g., order status updates)
- Collaborative apps (whiteboards, document editing)
- Multiplayer games
Example:
// Hub
public class ChatHub : Hub
{
public Task SendMessage(string user, string message) =>
Clients.All.SendAsync("ReceiveMessage", user, message);
}
// Program.cs
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chat");
app.Run();
What .NET engineers should know about SignalR:
- πΌ Junior: SignalR enables real-time server-to-client messaging; works over WebSockets or fallbacks.
- π Middle: Understand hubs, groups, scaling with Redis/Azure SignalR, and client SDKs (JavaScript, .NET, Java, etc.).
- π Senior: Architect for scale (backplanes), manage connection lifecycles, handle reconnection logic, secure hubs with auth/authorization, and optimize message payloads.
π Resources:
β What is the OutputCache middleware (.NET 7+), and how is it different from ResponseCaching?
OutputCache
(introduced in .NET 7) It is the next-generation server-side response caching middleware. It stores generated responses in memory (or other stores in the future) and serves them directly on subsequent requests without re-executing the endpoint. Unlike the older ResponseCaching
middleware, it:
- Works regardless of HTTP cache headers (you control caching in code).
- Supports rich policies (vary by query, route, headers, custom logic).
- Integrates cleanly with minimal APIs, MVC, and Razor Pages.
- Handles POST (and other non-GET) caching if you allow it.
- Has more predictable eviction and expiration options.

Example:
// Program.cs β basic OutputCache
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseOutputCache();
// Minimal API with 60s cache
app.MapGet("/products", () => new[] { "A", "B", "C" })
.CacheOutput(p => p.Expire(TimeSpan.FromSeconds(60))
.SetVaryByQuery("page", "size"));
app.Run();
Key differences β OutputCache vs ResponseCaching:
Feature | OutputCache (.NET 7+) | ResponseCaching |
---|---|---|
Trigger | Controlled in code (.CacheOutput(...) ) | Driven by HTTP headers (Cache-Control, Vary) |
Scope | Minimal APIs, MVC, Razor Pages | Mostly MVC and Razor Pages |
Varying | Query, route, headers, custom keys | Limited to Vary header |
POST support | Yes (if explicitly enabled) | No (GET/HEAD only) |
Expiration | Per-policy, absolute, sliding | Based on cache headers |
Eviction | Explicit per policy / programmatic | Automatic when headers expire |
Customization | High β policies, predicates, programmatic | Low β header-based only |
Recommended for new apps | β Yes | β Legacy, only if minimal caching needed |
What .NET engineers should know:
- πΌ Junior: Understand the difference between OutputCache and ResponseCaching.
- π Middle: Use OutputCache for per-endpoint policies; ResponseCaching only if you want to honor browser/HTTP cache headers directly.
- π Senior: Design cache keys carefully, combine with distributed caches for multi-instance scale-out, invalidate cache on data changes, and measure hit ratio to tune TTLs.
π Resources:
β How to enable response compression in ASP.NET Core?
ASP.NET Core supports Gzip, Brotli, and custom compression providers to reduce payload size and improve performance. You add the compression middleware and configure supported MIME types.
Example:
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/json" });
});
builder.Services.Configure<GzipCompressionProviderOptions>(o =>
o.Level = CompressionLevel.Fastest);
app.UseResponseCompression();
What .NET engineers should know:
- πΌ Junior: Know compression reduces response size to speed up loading.
- π Middle: Understand how to enable Gzip/Brotli and choose MIME types to compress.
- π Senior: Optimize compression settings, balance CPU cost vs performance gain, and monitor impact in production.
π Resources: Response compression in ASP.NET Core
β How to integrate OpenTelemetry for distributed tracing and metrics in ASP.NET Core?
OpenTelemetry lets you collect traces, metrics, and logs across distributed systems. You install OpenTelemetry.Extensions.Hosting
and configure tracing/metrics exporters (Jaeger, Zipkin, OTLP).
Example:
builder.Services.AddOpenTelemetry()
.WithTracing(t =>
{
t.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter();
})
.WithMetrics(m =>
{
m.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter();
});
What .NET engineers should know:
- πΌ Junior: Know how OpenTelemetry collects and sends app performance data.
- π Middle: Understand how to instrument ASP.NET Core with tracing and metrics, and connect to a backend like Jaeger or Grafana.
- π Senior: Design complete observability stacks, optimize sampling, correlate traces across microservices, and use telemetry for performance tuning.
π Resources: OpenTelemetry for .NET
β How to scale SignalR with Redis backplane or Azure SignalR Service
SignalR works typically in a single serverβs memory, so in multi-server setups, messages wonβt reach all clients.
You solve this with:
- Redis backplane β servers publish/subscribe messages via Redis.
- Azure SignalR Service β fully managed scale-out without managing Redis.
Example (Redis backplane):
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379");
Example (Azure SignalR Service):
builder.Services.AddSignalR()
.AddAzureSignalR("ConnectionString");
app.UseAzureSignalR(routes =>
{
routes.MapHub<ChatHub>("/chat");
});
What .NET engineers should know:
- πΌ Junior: Know SignalR needs a backplane or service to work across multiple servers.
- π Middle: Understand Redis and Azure SignalR options, and when each is appropriate.
- π Senior: Design scalable real-time systems, manage connection limits, and optimize performance for thousands of concurrent clients.
π Resources:
β How to add Polly resilience policies (retry, circuit breaker) with HttpClientFactory
?
ASP.NET Core integrates IHttpClientFactory
With Polly, you can add retries, circuit breakers, and more directly to your HTTP clients.
Example:
builder.Services.AddHttpClient("MyClient")
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))))
.AddTransientHttpErrorPolicy(p =>
p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
What .NET engineers should know:
- πΌ Junior: Know Polly helps handle transient HTTP failures with retries.
- π Middle: Understand how to configure retry and circuit breaker policies with HttpClientFactory.
- π Senior: Design advanced resilience strategies with fallback, timeout, bulkhead isolation, and monitor their effectiveness in production.
π Resources: Use Polly-based handlers
β How to benchmark and profile ASP.NET Core apps?
Its exist two common ways to measure benchmarks for .NET projects:
- dotnet-counters β real-time performance metrics (CPU, GC, requests/sec) for a running app.
- BenchmarkDotNet β a micro-benchmarking framework for measuring the performance of specific code paths.
Example (dotnet-counters):
dotnet tool install --global dotnet-counters
dotnet-counters monitor --process-id 1234
Example (BenchmarkDotNet):
[MemoryDiagnoser]
public class MyBenchmarks
{
[Benchmark]
public void TestMethod() => Thread.Sleep(10);
}
BenchmarkRunner.Run<MyBenchmarks>();
What .NET engineers should know:
- πΌ Junior: Know you can measure performance with built-in and third-party tools.
- π Middle: Understand when to use runtime profiling (dotnet-counters) vs code benchmarking (BenchmarkDotNet).
- π Senior: Profile production issues, analyze GC and thread usage, and design benchmarks that reflect real-world workloads.
π Resources:
Misc
Here, we cover how to ensure your application is reliable, portable, and ready for global users.

β How to test ASP.NET Core apps with WebApplicationFactory
for integration testing?
WebApplicationFactory<TEntryPoint>
from Microsoft.AspNetCore.Mvc.Testing
spins up a real in-memory server so you can test endpoints without deploying the app. Itβs often used with HttpClient
for end-to-end integration tests.
public class ProductsTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductsTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsOk()
{
var response = await _client.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
}
}
What .NET engineers should know:
- πΌ Junior: Know you can run real API tests without deploying using WebApplicationFactory.
- π Middle: Understand how to override configurations, mock dependencies, and reset state between tests.
- π Senior: Build complex integration test setups with multiple services, databases, and authentication flows.
π Resources: Integration tests in ASP.NET Core
β What are the best practices for deploying ASP.NET Core apps cross-platform?
ASP.NET Core runs on Windows, Linux, and macOS, so deployment is flexible.
Key practices:
- Use a self-contained deployment if you donβt want to rely on the target machineβs .NET runtime.
- Run in containers (Docker) for portability and consistent environments.
- Reverse proxy with Nginx or Apache on Linux for SSL termination and load balancing.
- Environment-specific configs via
appsettings.{Environment}.json
or environment variables. - CI/CD pipelines for automated build, test, and deployment.
Example (publish self-contained for Linux):
dotnet publish -c Release -r linux-x64 --self-contained true
What .NET engineers should know:
- πΌ Junior: Know .NET apps can run on multiple OSes without code changes.
- π Middle: Understand deployment options (framework-dependent vs self-contained, containers, reverse proxy).
- π Senior: Design cross-platform deployment pipelines, automate scaling, and optimize for cloud-native environments.
β How to make ASP.NET Core apps scalable and cloud-ready?
For scalability in the cloud, design your app to handle multiple instances without depending on local state.
Key practices:
- Statelessness β store session/data in a distributed cache (Redis, SQL), not in memory.
- Health checks β expose
/health
endpoints for load balancers and orchestrators. - Distributed cache β share cache across all instances for consistency.
- Horizontal scaling β run multiple app instances behind a load balancer.
Example (health checks + Redis cache):
builder.Services.AddHealthChecks()
.AddRedis("localhost:6379");
builder.Services.AddStackExchangeRedisCache(o =>
o.Configuration = "localhost:6379");
app.MapHealthChecks("/health");
What .NET engineers should know:
- πΌ Junior: Know that cloud apps should not store important data in local memory.
- π Middle: Understand how health checks and distributed caching help scaling.
- π Senior: Architect fully stateless services, implement resilience patterns and integrate with Kubernetes or cloud PaaS scaling features.
π Resources: Health checks in ASP.NET Core
β How to implement localization and globalization in ASP.NET Core (IStringLocalizer
)?
Localization lets your app display content in multiple languages, while globalization ensures it works with different cultures (dates, numbers, currencies).IStringLocalizer
retrieves localized strings from .resx
files without hardcoding them.
Example:
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");
app.UseRequestLocalization(new RequestLocalizationOptions
{
SupportedCultures = new[] { new CultureInfo("en"), new CultureInfo("fr") },
SupportedUICultures = new[] { new CultureInfo("en"), new CultureInfo("fr") },
DefaultRequestCulture = new RequestCulture("en")
});
public class HomeController : Controller
{
private readonly IStringLocalizer<HomeController> _localizer;
public HomeController(IStringLocalizer<HomeController> localizer) => _localizer = localizer;
public IActionResult Index() => Content(_localizer["Hello"]);
}
What .NET engineers should know:
- πΌ Junior: Know localization changes displayed text based on the user's language.
- π Middle: Understand how to configure
IStringLocalizer
with resource files and supported cultures. - π Senior: Build dynamic language-switching, manage translations at scale, and integrate with external localization platforms.
π Resources: Globalization and localization in ASP.NET Core