A Visual Guide to the ASP.NET Core Pipeline
Your Interview Guide to the ASP.NET Core Pipeline
'Can you explain the ASP.NET Core request pipeline?' This is one of the most common and important questions. Your answer shows you understand the entire request lifecycle.
The simple answer: 'The pipeline is a series of middleware components chained together. Each incoming HTTP request flows through this pipeline. Each middleware can inspect, modify, or act on the request. It can then either pass the request to the next middleware or 'short-circuit' and send a response directly.'
The 'Onion' Analogy:
Think of it as an onion. The request goes in through all the layers, hits the center (the endpoint), and then the response goes out through all the layers in reverse order.
Order is CRITICAL:
The order you add middleware in your Program.cs file is the order it executes. This is a common interview 'gotcha'.
// A simple Program.cs (ASP.NET Core 6+)
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 1. Exception Handling first, to catch errors from later middleware
app.UseExceptionHandler("/Error");
// 2. HTTPS Redirection, for security
app.UseHttpsRedirection();
// 3. Routing: Figures out which endpoint the request is for
app.UseRouting();
// 4. Authentication: Figures out who the user is
app.UseAuthentication();
// 5. Authorization: Figures out what the user is allowed to do
// (This must come after Authentication)
app.UseAuthorization();
// 6. Endpoint: The middleware that actually runs your code (e.g., a Controller action)
app.MapControllers();
app.Run();In this cluster, we'll dive into the key parts of this pipeline: Middleware, Dependency Injection, and Filters.
Middleware: How to Write Your Own
Interview Question: 'Have you ever written custom middleware?'
This is a great practical question. The answer is to describe the simple class pattern.
Answer: 'Yes. A custom middleware is a class that takes a RequestDelegate in its constructor, which is a reference to the 'next' middleware in the pipeline. It must have an InvokeAsync method that takes the HttpContext. Inside this method, you can run code before calling await _next(context), and after it, which lets you modify both the request and the response.'
Code Example: A Simple Logging Middleware
This is the classic example. This middleware will log the incoming request and how long it took to process.
// 1. The Middleware Class
public class RequestLoggingMiddleware {
private readonly RequestDelegate _next;
private readonly ILogger _logger;
// Get 'next' and any other services (like ILogger) via DI
public RequestLoggingMiddleware(RequestDelegate next, ILogger logger) {
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context) {
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try {
// --- Code before 'next' runs on the Request ---
_logger.LogInformation($"Handling request: {context.Request.Path}");
// 2. Call the next middleware in the pipeline
await _next(context);
// --- Code after 'next' runs on the Response ---
stopwatch.Stop();
_logger.LogInformation(
$"Request {context.Request.Path} finished in {stopwatch.ElapsedMilliseconds}ms " +
$"with status {context.Response.StatusCode}"
);
}
catch (Exception ex) {
// Handle exceptions from later middleware
_logger.LogError(ex, "An error occurred in the pipeline.");
stopwatch.Stop();
// ... re-throw or handle ...
throw;
}
}
}
// 3. You also need an extension method to make it easy to add
public static class RequestLoggingMiddlewareExtensions {
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder) {
return builder.UseMiddleware();
}
}
// 4. In Program.cs, you can now add it:
// var app = builder.Build();
// app.UseRequestLogging(); // <-- Add your custom middleware
// app.UseRouting();
// ... Dependency Injection Scopes: `Singleton` vs. `Scoped` vs. `Transient`
Interview Question: 'Explain the DI service lifetimes: Singleton, Scoped, and Transient.'
This is one of the most critical ASP.NET Core concepts. Your answer must be correct.
Answer: 'The lifetimes define how long a service object lives and when it is shared. It controls the 'lifecycle' of an injected service.'
1. AddSingleton()
- Lifetime: One instance for the entire application.
- How it works: The first time anyone requests this service, a new instance is created. That exact same instance is then given to every other component, in every request, for the entire lifetime of the app.
- When to use it: For lightweight, thread-safe services that hold global state. Examples: A logging configuration service, a singleton cache service, or app-wide settings.
- The Gotcha: You must ensure it's thread-safe.
builder.Services.AddSingleton(); 2. AddScoped()
- Lifetime: One instance per client request (per HTTP request).
- How it works: When a new HTTP request comes in, the DI container creates a new instance. That same instance is shared among all components within that single request. When a different HTTP request comes in, it gets its own new instance.
- When to use it: This is the most common lifetime. It's the default for Entity Framework's
DbContext. Use it for your repositories, unit of work, or any service that needs to share data or state during a single request, but must be isolated from other requests.
builder.Services.AddScoped();
builder.Services.AddDbContext(...); // This is Scoped by default 3. AddTransient()
- Lifetime: A new instance every single time it's requested.
- How it works: Every time a component's constructor asks for this service, the DI container creates a brand new one. If 5 services in a single request all depend on it, 5 separate instances will be created.
- When to use it: For lightweight, stateless utility services that have no dependencies of their own. Examples: An email sender (
IEmailSender) or a simple calculator service.
builder.Services.AddTransient(); Filters: How They Differ from Middleware
Interview Question: 'What's the difference between Middleware and a Filter?'
This is a more advanced question. Answering it well shows you understand the layers of the framework.
Answer: 'Middleware is 'lower level'. It operates on the HttpContext and doesn't know about MVC or API concepts like 'actions' or 'model binding'. Filters are 'higher level'. They are part of the MVC/API framework and run at specific stages within the endpoint execution pipeline. This means a filter can access things like model-bound arguments and action results, while middleware cannot.'
Analogy: The Building vs. The Office
- Middleware: Is the security guard at the front door of the building (
app.Use...). They check everyone who comes in (HttpContext). They don't know or care which office you're going to. (e.g., Logging, CORS, Authentication). - Filter: Is the security guard at the door of your specific office (
[MyActionFilter]). They run after routing has already decided you're going to 'Sales Department, Office 101'. They can see who you're there to meet (Action arguments) and what you leave with (Action result).
Code Example: An Action Filter
You can't do this with middleware. Notice it has `OnActionExecuting` (before the action) and `OnActionExecuted` (after the action). It has access to the ActionExecutingContext, which has the ActionArguments.
using Microsoft.AspNetCore.Mvc.Filters;
// This filter logs the arguments passed to an action
public class LogActionArgsFilter : IActionFilter {
private readonly ILogger _logger;
public LogActionArgsFilter(ILogger logger) {
_logger = logger;
}
// Runs before the action method
public void OnActionExecuting(ActionExecutingContext context) {
// A filter can access MVC-specific context!
string actionName = context.ActionDescriptor.DisplayName;
var arguments = context.ActionArguments;
_logger.LogInformation($"Running action {actionName} with args: {arguments}");
}
// Runs after the action method
public void OnActionExecuted(ActionExecutedContext context) {
// A filter can even inspect or modify the Result!
if (context.Result is OkObjectResult okResult) {
_logger.LogInformation($"Action finished with status 200 and value: {okResult.Value}");
}
}
}
// You then register this filter in Program.cs
// builder.Services.AddControllers(options => {
// options.Filters.Add();
// });
// Or apply it as an attribute:
// [ApiController]
// [Route("[controller]")]
// [ServiceFilter(typeof(LogActionArgsFilter))] // <-- Apply filter
// public class MyController : ControllerBase { ... } The simple answer: 'Middleware is global and works on HttpContext. Filters are part of MVC/API and work on the ActionContext, so they can run code before/after an action, inspect model binding, and modify action results.'


