ASP.NET Core middleware is one of those concepts that every .NET developer uses, but many developers don't fully understand what happens behind the scenes.
Authentication, authorization, exception handling, logging, CORS, HTTPS redirection, rate limiting, and even your own custom logic can all be implemented through middleware.
Once you understand how middleware works, ASP.NET Core applications become much easier to design, debug, and maintain.
In this tutorial, we'll go beyond the basic definition and explore 10 important ASP.NET Core middleware concepts with practical examples, including how middleware executes, how to create custom middleware, how Use, Run, and Map differ, how ordering affects your application, and how to avoid common mistakes.
Table of Contents
- What Is Middleware?
- How the Middleware Pipeline Works
-
Use()vsRun()vsMap() - Creating Custom Middleware
- Middleware Ordering
- Exception Handling Middleware
- Request Logging Middleware
- Authentication and Authorization Middleware
- Conditional Middleware
- Short-Circuiting the Pipeline
- Middleware with Dependency Injection
- Common Middleware Mistakes
- Performance Best Practices
- Middleware Interview Questions
- Frequently Asked Questions
- Final Thoughts
1. What Is Middleware?
Middleware is a component that participates in handling an HTTP request and response.
You can think of middleware as a series of checkpoints through which every request passes.
For example, a request might travel through:
Client
↓
Exception Handling
↓
HTTPS Redirection
↓
Logging
↓
Authentication
↓
Authorization
↓
Routing
↓
Controller
↓
Response
Each middleware component can:
- Inspect the request
- Modify the request
- Perform some operation
- Call the next middleware
- Modify the response
- Stop the request from continuing
This makes middleware extremely powerful.
2. How the Middleware Pipeline Works
Let's start with a simple example.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("Before next middleware");
await next();
Console.WriteLine("After next middleware");
});
app.Run(async context =>
{
Console.WriteLine("Endpoint");
await context.Response.WriteAsync("Hello World!");
});
app.Run();
When a request arrives, the output is:
Before next middleware
Endpoint
After next middleware
Notice something important.
The middleware doesn't simply execute from top to bottom.
The call to:
await next();
passes execution to the next middleware.
When that middleware finishes, execution returns to the previous middleware.
You can visualize it like this:
Middleware A
↓
Middleware B
↓
Endpoint
↑
Middleware B
↑
Middleware A
This is sometimes called the middleware pipeline or onion model.
Understanding this behavior is essential when working with logging, authentication, exception handling, and response manipulation.
3. Use() vs Run() vs Map()
One of the most common ASP.NET Core interview questions is the difference between Use(), Run(), and Map().
Use()
Use() adds middleware that can call the next component.
app.Use(async (context, next) =>
{
Console.WriteLine("Before");
await next();
Console.WriteLine("After");
});
Because it receives next, it can continue the pipeline.
Run()
Run() adds terminal middleware.
app.Run(async context =>
{
await context.Response.WriteAsync("Hello");
});
There is no next parameter.
Once this middleware executes, the pipeline ends.
For example:
app.Use(async (context, next) =>
{
Console.WriteLine("Middleware 1");
await next();
});
app.Run(async context =>
{
Console.WriteLine("Terminal middleware");
});
Anything registered after Run() won't execute.
Map()
Map() allows you to create a separate pipeline based on the request path.
app.Map("/admin", adminApp =>
{
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin Area");
});
});
Now:
/admin
will enter the mapped pipeline.
This is useful when different URL paths require completely different processing.
4. Creating Custom Middleware
One of the most useful skills for an ASP.NET Core developer is creating custom middleware.
Suppose you want to measure how long every request takes.
You could create:
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
public RequestTimingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
await _next(context);
stopwatch.Stop();
Console.WriteLine(
$"{context.Request.Path} took {stopwatch.ElapsedMilliseconds} ms");
}
}
You can register it with:
app.UseMiddleware<RequestTimingMiddleware>();
Now every request passes through your middleware.
Why Use Custom Middleware?
Custom middleware is useful for cross-cutting concerns such as:
- Request logging
- Correlation IDs
- Exception handling
- Performance measurement
- Security headers
- Request validation
- Custom authentication
- Tenant identification
- Audit logging
The important principle is:
Middleware is best suited for logic that applies across many requests.
Don't put business-specific logic into middleware when it belongs inside a service or controller.
5. Middleware Ordering Matters
This is one of the most important concepts to understand.
Consider:
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
The order is intentional.
Authentication establishes who the user is.
Authorization then determines whether that user is allowed to access the resource.
Finally, the request reaches the endpoint.
Conceptually:
Request
↓
Authentication
↓
Authorization
↓
Controller
If you change the order incorrectly, your application may behave unexpectedly.
For example:
app.UseAuthorization();
app.UseAuthentication();
This is generally incorrect because authorization may execute before the user has been authenticated.
General rule
When working with middleware, always ask:
"Does this middleware depend on something that runs before it?"
That question helps prevent many pipeline-related bugs.
6. Exception Handling Middleware
Production applications should not expose raw exception details to users.
Instead of allowing an exception to travel directly to the client, use exception-handling middleware.
ASP.NET Core provides built-in support:
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}
You can also implement your own middleware.
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
Console.WriteLine(ex);
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
message = "An unexpected error occurred."
});
}
}
This gives you a central place to handle unexpected failures.
Important
Don't return sensitive exception details in production.
Avoid responses such as:
{
"error": "SqlException: Login failed for user..."
}
Instead, return a safe message and log the detailed exception internally.
7. Request Logging Middleware
Logging every request can be extremely useful when troubleshooting production applications.
A simple middleware might look like this:
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
await _next(context);
stopwatch.Stop();
_logger.LogInformation(
"HTTP {Method} {Path} returned {StatusCode} in {Elapsed}ms",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
stopwatch.ElapsedMilliseconds);
}
}
This can produce logs such as:
HTTP GET /api/products returned 200 in 42ms
This is much more useful than randomly placing Console.WriteLine() statements throughout your application.
8. Authentication and Authorization Middleware
Authentication and authorization are related but different concepts.
Authentication
Authentication answers:
Who are you?
For example:
User → JWT Token → Authentication → User Identity
Authorization
Authorization answers:
Are you allowed to access this resource?
For example:
Authenticated User
↓
Required Role = Admin
↓
Allowed / Forbidden
In ASP.NET Core, you'll commonly see:
app.UseAuthentication();
app.UseAuthorization();
And then protect endpoints with:
[Authorize]
public IActionResult GetOrders()
{
return Ok();
}
Or with roles:
[Authorize(Roles = "Admin")]
public IActionResult DeleteUser(int id)
{
return Ok();
}
This separation is fundamental when designing secure APIs.
9. Conditional Middleware
Sometimes you don't want middleware to execute for every request.
ASP.NET Core provides ways to conditionally add middleware.
For example:
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
branch =>
{
branch.UseMiddleware<RequestTimingMiddleware>();
});
Now the middleware only runs for requests beginning with:
/api
You can also use MapWhen() when you want to create a completely separate pipeline.
app.MapWhen(
context => context.Request.Query.ContainsKey("debug"),
branch =>
{
branch.Run(async context =>
{
await context.Response.WriteAsync("Debug pipeline");
});
});
Conditional middleware can be useful when different parts of your application require different processing.
10. Short-Circuiting the Pipeline
Middleware doesn't always have to call next().
It can stop the pipeline completely.
For example:
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-API-KEY"))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API key required.");
return;
}
await next();
});
If the API key doesn't exist, the request ends immediately.
The controller will never execute.
This is called short-circuiting.
Short-circuiting is useful for:
- Authentication checks
- API key validation
- IP restrictions
- Maintenance mode
- Rate limiting
- Request validation
However, don't overuse it.
If the logic belongs to authorization, validation, or business logic, use the appropriate ASP.NET Core mechanism instead of creating unnecessary custom middleware.
11. Middleware with Dependency Injection
Middleware can also use services registered with ASP.NET Core's dependency injection container.
For example:
public class AuditMiddleware
{
private readonly RequestDelegate _next;
public AuditMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(
HttpContext context,
IAuditService auditService)
{
await _next(context);
await auditService.RecordAsync(
context.Request.Path,
context.Response.StatusCode);
}
}
Notice that IAuditService is injected into InvokeAsync().
This is useful when middleware needs application services.
For example:
builder.Services.AddScoped<IAuditService, AuditService>();
Then the middleware can use it.
Important Lifetime Consideration
Be careful when injecting scoped services into middleware.
Conventional middleware instances can live longer than a single request, so injecting scoped services into the middleware constructor can create lifetime problems.
Prefer injecting request-scoped services into InvokeAsync() when appropriate.
12. Common Middleware Mistakes
Let's look at mistakes that frequently appear in real applications.
Mistake 1: Forgetting await next()
Consider:
app.Use(async (context, next) =>
{
Console.WriteLine("Logging");
// next() is never called
});
The pipeline stops here.
Unless the middleware intentionally short-circuits the request, this is a bug.
Mistake 2: Incorrect Middleware Order
For example:
app.UseAuthorization();
app.UseAuthentication();
Authentication should normally happen before authorization.
Always understand the dependencies between middleware components.
Mistake 3: Putting Business Logic in Middleware
Avoid doing this:
if (order.Total > 10000)
{
// complex business rules
}
Middleware should generally handle cross-cutting concerns.
Business rules belong in appropriate application/domain services.
Mistake 4: Logging Sensitive Information
Don't blindly log:
- Passwords
- Access tokens
- Refresh tokens
- Credit card information
- Sensitive personal information
Logging should help diagnose problems without creating a security problem.
Mistake 5: Doing Expensive Work on Every Request
Middleware runs frequently.
If your application receives 10,000 requests, middleware may execute 10,000 times.
Avoid unnecessary database calls, network calls, or expensive calculations in globally registered middleware.
13. Middleware Performance Best Practices
Performance matters because middleware can affect every request.
Keep Middleware Lightweight
Good middleware:
Request
↓
Small operation
↓
next()
Avoid unnecessarily expensive processing.
Avoid Blocking Calls
Prefer:
await service.ProcessAsync();
instead of synchronous blocking operations.
Avoid patterns such as:
service.ProcessAsync().Result;
or:
service.ProcessAsync().Wait();
These can cause thread-pool starvation and performance problems.
Use Structured Logging
Instead of:
_logger.LogInformation(
$"Request {path} took {time}ms");
Prefer structured logging:
_logger.LogInformation(
"Request {Path} took {Elapsed}ms",
path,
time);
Structured logs are easier to search and analyze in logging platforms.
14. Middleware vs Filters
A common interview question is:
What's the difference between middleware and MVC filters?
Middleware operates at the HTTP pipeline level.
Filters are more closely associated with MVC/controller or endpoint execution.
Think of it like:
HTTP Request
↓
Middleware
↓
Routing
↓
Endpoint
↓
MVC Filters
↓
Controller
Use middleware for application-wide concerns.
Use filters when the behavior is specifically related to MVC/controller or action execution.
15. Middleware Interview Questions
Here are some questions worth knowing for .NET interviews.
1. What is middleware in ASP.NET Core?
Middleware is software that participates in processing HTTP requests and responses within the ASP.NET Core pipeline.
2. What is next()?
next() passes control to the next middleware component.
3. What happens if next() isn't called?
The pipeline is short-circuited unless the middleware intentionally handles the response itself.
4. What is the difference between Use() and Run()?
Use() can call the next middleware.
Run() is terminal middleware and doesn't continue the pipeline.
5. Why does middleware order matter?
Because middleware executes in the order it is registered, and some components depend on others running first.
6. How do you create custom middleware?
Create a class that accepts RequestDelegate and exposes an Invoke or InvokeAsync method.
7. What is short-circuiting?
Stopping the request pipeline before subsequent middleware executes.
8. When should middleware be used?
For cross-cutting concerns such as logging, exception handling, authentication, security headers, and request processing.
Frequently Asked Questions
Is middleware executed for every request?
Middleware registered globally generally participates in every request unless the pipeline is branched or the middleware itself chooses to short-circuit or skip processing.
Can middleware modify the response?
Yes.
Middleware can inspect or modify the response before or after calling the next component.
Can middleware access services?
Yes. Middleware can use ASP.NET Core dependency injection.
Should business logic be placed in middleware?
Generally, no. Middleware should focus on cross-cutting concerns rather than application-specific business rules.
Can middleware stop a request?
Yes. Middleware can short-circuit the pipeline by not calling next() and returning a response directly.
Final Thoughts
Middleware is one of the foundations of ASP.NET Core.
Once you understand how the pipeline works, many ASP.NET Core features become easier to understand.
The most important concepts to remember are:
- Middleware forms a request-processing pipeline.
-
Use()can continue the pipeline. -
Run()is terminal. -
Map()can create branches. - Middleware order matters.
- Middleware can short-circuit requests.
- Custom middleware is useful for cross-cutting concerns.
- Authentication should generally run before authorization.
- Middleware should remain lightweight.
- Business logic should live in appropriate services.
If you're preparing for a .NET interview, don't just memorize these definitions. Build a small ASP.NET Core API and implement your own logging, exception handling, API-key validation, and request-timing middleware.
That's when the pipeline really starts to make sense.
🚀 Explore More Free Developer Tools
As developers, we often switch between different tools while building and debugging applications. Formatting API responses, decoding JWTs, comparing configuration files, generating UUIDs, or converting data shouldn't require opening several different websites.
That's one reason I built ToolBenchApp.
ToolBenchApp is a growing collection of free, browser-based developer tools designed to make everyday development tasks faster and simpler.
Some of the available tools include:
- JSON Formatter & Validator
- JWT Decoder
- JSON ↔ XML Converter
- JSON ↔ CSV Converter
- Base64 Encoder/Decoder
- UUID Generator
- URL Encoder/Decoder
- SQL Formatter
- YAML Formatter
- Text Difference Checker
- HTML Formatter
- Regex Tester
If you're working with ASP.NET Core APIs, tools like the JSON Formatter, JWT Decoder, and Text Diff Checker can be particularly useful during development and debugging.
I'm continuously adding new utilities based on what developers actually need, so if there's a tool you'd like to see on ToolBenchApp, feel free to share your idea.
Happy coding! 🚀
Top comments (0)