- In dotnet, a
Middleware
is a piece of code that runs in the request pipeline. - Middlewares are put together in a sequence, and their order matters.
- When a request is made, it goes through each of the middleware.
- Response flow back in reverse order through the middleware.
- Each middleware has capability to modify the request or short circuits the pipeline.
- Each middleware can change the response before it’s sent to the client.
Updated at
- 25-june-2025
- 26-june-2025
- 01-July-2025
Built-in middlewares
Here are some of the built-in middlewares.
- UseHttpsRedirection(): Redirects http requests to https.
- UseHsts(): Add http strict Transport security headers.
- UseCors(): Enable cross origin request
- UseAuthorization(), UseAuthentication(), UseRouting(), MapControllers(), MapEndpoints().
- Etc etc..
Configuring middleware
They are configured in the Program.cs
using the WebApplicationBuilder
.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Order matters!
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Creating a custom middleware
There are three ways to create the custom middlewares:
1. Inline middleware
app.Use(async (context, next) =>
{
Console.WriteLine("This is middleware");
await next(context);
});
Note: Middleware do not run until you make any http
request.
Let’s add an endpoint.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// endpoint
app.MapGet("/", () =>
{
return "Hello";
});
app.Use(async (context, next) =>
{
Console.WriteLine("This is middleware");
await next(context);
});
app.Run();
If you run the application and call the endpoint http://localhost:5000
. You will get Hello
as the response. If you open your console, you will notice these logs.
This is middleware
Before moving to 2nd and 3rd way to create a middleware, let’s explore more about middlewares.
Exploring more about middlewares
Let’s explore some more things about middlewares what we have discussed in the beginning.
app.Use(async (context, next) =>
{
Console.WriteLine("===> Middleware 1 before next() (during req)");
await next(context);
Console.WriteLine("===> Middleware 1 after next() (during response)");
});
app.Use(async (context, next) =>
{
Console.WriteLine("===> Middleware 2 before next() (during req)");
await next(context);
Console.WriteLine("===> Middleware 2 after next() (during response)");
});
Terminal logs:
===> Middleware 1 before next() (during req)
===> Middleware 2 before next() (during req)
===> Middleware 2 after next() (during response)
===> Middleware 1 after next() (during response)
So what does this show us?
- Code block before
next()
is executed while request. - Code block after
next()
is executed while response. - Response flow back in reverse order, as we have discussed in the beginning of the blog post.
Modifying request and response
We can modify the request and response in the middleware. In this example, we are adding a custom request header (X-Custom-Req) and a custom response header (X-Custom-Response) and also appending some text in the response.
app.Use(async (context, next) =>
{
// adding a request header
context.Request.Headers.Append("X-Custom-Req", "Custom Req");
// adding a response header
context.Response.Headers.Append("X-Custom-Response", "Custom Response");
// Request Headers
Console.WriteLine("\nREQUEST HEADERS:");
foreach (var header in context.Request.Headers.OrderBy(h => h.Key))
{
Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value.ToArray())}");
}
context.Response.ContentType = "text/plain";
await next(context);
// Response Headers
Console.WriteLine("\nRESPONSE HEADERS:");
foreach (var header in context.Response.Headers.OrderBy(h => h.Key))
{
Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value.ToArray())}");
}
Console.WriteLine("=" + new string('=', 50));
await context.Response.WriteAsync("\nAppended in middleware 1...\n");
});
app.Use(async (context, next) =>
{
await next(context);
await context.Response.WriteAsync("\nAppended in middleware 2");
});
Request endpoint:
app.MapGet("/greetings", () =>
{
return "Hello world";
});
Response:
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain
Date: Wed, 25 Jun 2025 16:51:51 GMT
Server: Kestrel
Transfer-Encoding: chunked
X-Custom-Response: Custom Response
Hello world
Appended in middleware 2
Appended in middleware 1...
Logs in terminal:
REQUEST HEADERS:
Accept-Encoding: gzip, deflate
Connection: close
Host: localhost:5009
User-Agent: vscode-restclient
X-Custom-Req: Custom Req
RESPONSE HEADERS:
Connection: close
Content-Type: text/plain
Date: Wed, 25 Jun 2025 13:51:51 GMT
Server: Kestrel
Transfer-Encoding: chunked
X-Custom-Response: Custom Response
===================================================
2. Conventional middlewares
namespace MiddleWareDemo;
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)
{
_logger.LogInformation($"Request {context.Request.Method} {context.Request.Path}");
await _next(context);
_logger.LogInformation($"Response: {context.Response.StatusCode}");
}
}
In Program.cs
app.UseMiddleware<RequestLoggingMiddleWare>();
Key points to consider about conventional middleware
- It is registered as singleton, when application runs for the first time.
- You can inject transient services to the middleware constructor, but can not inject the scoped services in it. e.g:
// Program.cs
builder.Services.AddScoped<ISomeService, SomeService>(); // Scoped service
// Middleware
public class RequestLoggingMiddleWare
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleWare> _logger;
private readonly ISomeService _someService;
public RequestLoggingMiddleWare(RequestDelegate next, ILogger<RequestLoggingMiddleWare> logger, ISomeService someService)
{
_next = next;
_logger = logger;
_logger.LogInformation("=====> Conventional middleware instantiated");
_someService = someService;
}
// rest of the code
}
This application will throw an error System.InvalidOperationException: Cannot resolve scoped service 'MiddleWareDemo.ISomeService' from root provider.
3. Factory-Based middlewares
- For this we need to implement
IMiddleware
interface. - It has only one method
InvokeAsync()
(as of .NET 9.0). - Major benefit for me is, it is a strongly typed middleware. I don’t need to remember the name of method (i.e InvokeAsync), that leads to less error prone code.
- It is
activated per http request
which allows us to inject scoped services in the middleware.
Example:
public class FactoryBasedMiddleware : IMiddleware
{
private readonly ILogger<FactoryBasedMiddleware> _logger;
public FactoryBasedMiddleware(ILogger<FactoryBasedMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
_logger.LogInformation("Before request");
await next(context);
_logger.LogInformation("After request");
}
}
In Program.cs
builder.Services.AddTransient<FactoryBasedMiddleware>();
app.UseMiddleware<FactoryBasedMiddleware>();
📢 Note that, unlike the conventional middleware, you need to register it as a transient
or scoped
lifetime.
Key points to consider about factory based middlewares
The factory-based middleware is registered as scoped or transient service (unlike conventional middleware, which registered as singleton).
Which means factory-based middleware will be activated per http request.
You can inject transient and scoped services to it (Unlike conventional middleware, which do not allow scoped service injection)
Terminal middleware
All of the above middlewares are non-terminal middlewares. They are chained together. They pass context to the next middleware in the sequence.
Terminal middleware is the final middleware of the pipeline. It is responsible for sending response back to client.
var app = builder.Build();
app.Map("/", () =>
{
return "Hello";
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from terminal middleware!");
// This ends the pipeline
});
app.Run();
If we run localhost:5000
, the page will display Hello from terminal middleware!
. It will override the response Hello
.
Branching the middleware pipeline
There might be a situation, when you want to execute separate middleware pipeline on certain paths or on certain scenarios.
Map
Map
branches on the basis of request path.
In the example below, if the request starts with /admin
, certain middlewares will be executed.
app.Map("/admin", HandleMapAdmin);
app.Run();
void HandleMapAdmin(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
Console.WriteLine("===> Middleware for admin");
await next(context);
});
app.Run(async context =>
{
await context.Response.WriteAsync("Response from admin branch");
});
}
If you make a request http://localhost:5285/
~$ curl http://localhost:5285/
===> main pipeline
If requests are like /admin
or /admin/blah
(it should starts with /admin
), you will the similar response (Response from admin branch
).
~$ curl http://localhost:5285/admin
Response from admin branch
~$ curl http://localhost:5285/admin/blah
Response from admin branch
Map can not join the main pipeline.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/admin", HandleMapAdmin);
app.Run(async context =>
{
await context.Response.WriteAsync("===> main pipeline");
});
app.Run();
void HandleMapAdmin(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
Console.WriteLine("===> Middleware for admin");
await next(context);
});
}
Curl req:
~$ curl http://localhost:5285/admin -v
* Host localhost:5285 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5285...
* Connected to localhost (::1) port 5285
* using HTTP/1.x
> GET /admin HTTP/1.1
> Host: localhost:5285
> User-Agent: curl/8.12.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Length: 0
< Date: Thu, 26 Jun 2025 15:32:53 GMT
< Server: Kestrel
<
* Connection #0 to host localhost left intact
If it has joined the main branch it would have gave the response ===> main pipeline
instead of 404 not found
status.
MapWhen
Branches the request pipeline based on the result of the given predicate.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapWhen(context => context.Request.Query.ContainsKey("page"),
Handle);
app.Run(async context =>
{
await context.Response.WriteAsync("===> main pipeline");
});
app.Run();
void Handle(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var page = context.Request.Query["page"];
Console.WriteLine($"===> Page = {page}");
await next(context);
});
app.Run(async context =>
{
await context.Response.WriteAsync("Response from branch");
});
}
Request 1:
~$ curl http://localhost:5285/
===> main pipeline
Request 2:
curl http://localhost:5285/something?page=22
Response from branch
It went to the different branch of pipeline when it gets the query parameter page.
Note: MapWhen can not rejoin the main pipeline.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapWhen(context => context.Request.Query.ContainsKey("page"),
Handle);
app.Run(async context =>
{
await context.Response.WriteAsync("===> main pipeline");
});
app.Run();
void Handle(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var page = context.Request.Query["page"];
Console.WriteLine($"===> Page = {page}");
await next(context);
});
}
Note that, there is not any terminal middleware in Handle method, request flow must go the next middleware. We are assuming that it will go the next middleware defined in the main pipeline. Would it?? Theoretically yes… So let’s find out.
~$ curl http://localhost:5285/something?page=22 -v
* Host localhost:5285 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5285...
* Connected to localhost (::1) port 5285
* using HTTP/1.x
> GET /something?page=22 HTTP/1.1
> Host: localhost:5285
> User-Agent: curl/8.12.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Length: 0
< Date: Thu, 26 Jun 2025 15:34:04 GMT
< Server: Kestrel
<
* Connection #0 to host localhost left intact
As you can see we are getting 404 not found
, but we should get 200 OK
status code and ===> main pipeline
as a response body.
That’s clearly indicates that, MapWhen
don’t re-join the main pipeline.
UseWhen
Let’s replace MapWhen withUseWhen .
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWhen(context => context.Request.Query.ContainsKey("page"),
Handle);
app.Run(async context =>
{
await context.Response.WriteAsync("===> main pipeline");
});
app.Run();
void Handle(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var page = context.Request.Query["page"];
Console.WriteLine($"===> Page = {page}");
await next(context);
});
}
Let’s make a curl request
~$ curl http://localhost:5285/something?page=22
===> main pipeline