Introduction
The first part of this series explained what an ASP.NET Core middleware is and how it plays a central role when processing HTTP requests.
There are three ways to write a middleware component. Very simple functionality can be formulated inline by using lambda expressions. You'll want to outsource the envisioned functionality in more complex situations into a separate class. These types must then either follow a convention or implement an interface so they can be instantiated during runtime.
This is part two of a five-part series that covers inline middleware.
Articles in this series
Part 1 - Introduction to ASP.NET Core middleware
Part 2 - Writing inline ASP.NET Core middleware using lambda expressions
Part 3 - Writing convention-based ASP.NET Core middleware
Part 4 - Writing factory-based ASP.NET Core middleware
Part 5 - Testing ASP.NET Core middleware
Inline middleware using lambda expressions
Most of the time, when writing an ASP.NET Core application, you'll deal with writing Razor pages or API controllers. However, there are situations when the full functionality of an API controller is not required.
This could be the case if all you want is to invoke an action (aka a poor man's IPC) or you need a simple endpoint to return the current weekday. Also, adding an HTTP header could be named as a legit use case.
For such purposes, a fully-fledged API controller could be written. But there are shorter alternatives available, such as using the extension methods Run()
, Map()
and Use()
which are part of the Microsoft.AspNetCore.Builder
namespace. Let's have a closer look.
Run extension method
The run extension method allows defining middleware components inline and adds it to the HTTP request pipeline.
public static void Run(this IApplicationBuilder app, RequestDelegate handler);
Signature Run() extension method
By accessing the RequestDelegate
parameter, your middleware can read from the HTTP context, which carries all request details.
// A function that can process an HTTP request
public delegate Task RequestDelegate(HttpContext context);
Signature RequestDelegate
In the example below, a request delegate is passed to Run()
as an anonymous method. The middleware plays Rock, Paper Scissors with a caller and returns the result as an HTTP response body. Also, it adds the outcome to a custom header X-Rochambeau
.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Define in-line middleware
app.Run(async (HttpContext context) =>
{
var items = new string[] { "rock", "paper", "scissors" };
var result = items[new Random().Next(items.Length)];
// Add custom header
context.Response.Headers.Add("X-Rochambeau", result);
// Write response body
await context.Response.WriteAsync($"Rochambeau-Outcome: {result}");
});
// Poor delegate, you'll never see an HTTP request :(
app.Run(async context =>
{
Debug.WriteLine("You'll never see me!");
});
app.Run();
Inline middleware example
This type of middleware always returns a response and represents a terminating endpoint that short-circuits the pipeline. Since the order in which middleware components are registered is important, any subsequent middleware will not be executed. This implies that the call to Run()
should be at the end of the pipeline configuration.
❯ curl --include https://localhost:7226
HTTP/1.1 200 OK
Date: Mon, 03 Jan 2022 22:07:03 GMT
Server: Kestrel
Transfer-Encoding: chunked
X-Rochambeau: rock
Rochambeau-Outcome: rock
❯ curl --include https://localhost:7226/foobar
HTTP/1.1 200 OK
Date: Mon, 03 Jan 2022 22:07:04 GMT
Server: Kestrel
Transfer-Encoding: chunked
X-Rochambeau: scissors
Rochambeau-Outcome: scissors
Calling the middleware
Reasonable usage of Run()
is limited due to the characteristics mentioned. However, it has its place; for example, when a simple endpoint is required that should always return a response, regardless of the request path.
However, it is more common to respond to certain request paths individually. The Map
and Use
methods are much better suited for this.
Map & MapWhen extension method
In contrast to the Run
method, Map
& MapWhen
allows the pipeline to be split into several independent execution branches. The decision on which path to execute is based on the query path and predicate. This allows for different behavior for different branches of the pipeline.
// Branches the request pipeline based on matches of the given request path.
// If the request path starts with the given path, the branch is executed.
public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)
Signature Map() extension
In addition to the path, Map
expects an Action
delegate that takes an IApplicationBuilder
as parameter.
The example below mimics the ASP.NET Core Health-Check build-in middleware, which provides a configurable endpoint that can be used to monitor the health of an application.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Create branch
app.Map("/health", (IApplicationBuilder branchBuilder) =>
{
// Terminal middleware
branchBuilder.Run(async context =>
{
await context.Response.WriteAsync("Healthy");
});
});
app.Map("/anotherbranch", (IApplicationBuilder branchBuilder) =>
{
branchBuilder.UseStaticFiles();
// Terminal middleware
branchBuilder.Run(async context =>
{
await context.Response.WriteAsync("Terminated anotherbranch!");
});
});
// Terminal middleware
app.Run(async context =>
{
await context.Response.WriteAsync("Terminated main branch");
});
app.Run();
Map() usage example
As each execution path is independent, a request travels through either one path or the other - but never through both. This fact implies that middleware added to a path-specific pipeline is only available there. The StaticFileMiddleware
from the example above is only used in the second branch /anotherbranch
.
❯ curl https://localhost:7226/health
Healthy
❯ curl https://localhost:7226/health/foobar
Healthy
❯ curl https://localhost:7226/anotherbranch
Terminated anotherbranch
❯ curl https://localhost:7226/
Terminated main branch
❯ curl https://localhost:7226/foobar
Terminated main branch
Calling the Map() example
The Map
method creates a new pipeline using the ApplicationBuilder
. As a result, the request path changes from the middleware's point of view.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/branch1", applicationBuilder =>
{
applicationBuilder.Run(async context =>
{
var path = context.Request.Path;
var pathBase = context.Request.PathBase;
await context.Response.WriteAsync($"Path: {path} PathBase: {pathBase}");
});
});
app.Run(async context =>
{
var path = context.Request.Path;
var pathBase = context.Request.PathBase;
await context.Response.WriteAsync($"Path: {path} PathBase: {pathBase}");
});
app.Run();
Invoking the example demonstrates that the path has been changed by Map
. The original path base gets cached.
❯ curl https://localhost:7014/branch1/segment1
Path: /segment1 PathBase: /branch1
❯ curl https://localhost:7014/anotherbranch/somesegment
Path: /anotherbranch/somesegment PathBase:
Invocation
In situations that require more complex structures, Map
can also be nested. Although this is technically possible, it usually makes little sense. Instead, you will want to use a full-fledged API controller in such cases.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Create branch
app.Map("/health", (IApplicationBuilder branchBuilder) =>
{
// Create sub-branch
branchBuilder.Map("/ping", (IApplicationBuilder anotherBranchBuilder) =>
{
// Terminal middleware
anotherBranchBuilder.Run(async (HttpContext context) =>
{
await context.Response.WriteAsync("pong");
});
});
// Terminal middleware
branchBuilder.Run(async context =>
{
await context.Response.WriteAsync("Healthy");
});
});
// Terminal middleware
app.Run(async context =>
{
await context.Response.WriteAsync("Terminus");
});
app.Run();
Nested Map() calls
❯ curl https://localhost:7226/health
Healthy
❯ curl https://localhost:7226/health/foo
Healthy
❯ curl https://localhost:7226/health/ping
pong
❯ curl https://localhost:7226/health/ping/foo
pong
❯ curl https://localhost:7226/
Terminus
Invocation of the example above
In addition to Map
, there is also a modified variant, MapWhen
. This allows conditional branching based on a predicate and the state of the received HttpContext
.
// Branches the request pipeline based on the result of the given predicate
public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration);
Signature of MapWhen()
In the example below branching only occurs, if an HTTP request contains the header X-Custom-Header
.
Func<HttpContext, bool> condition = (HttpContext context) =>
{
return context.Request.Headers.ContainsKey("X-Custom-Header");
};
app.MapWhen(condition, (IApplicationBuilder branchBuilder) =>
{
branchBuilder.Run(async (HttpContext context) =>
{
await context.Response.WriteAsync("Request contains X-Custom-Header");
});
});
Conditional branching with MapWhen()
A condensed version could look like this
app.MapWhen(context => context.Request.Headers.ContainsKey("X-Custom-Header"), branchBuilder =>
{
branchBuilder.Run(async context => await context.Response.WriteAsync("Request contains X-Custom-Header"));
});
MapWhen() with predicate
An alternative version to the path-based branching could look as follows
app.MapWhen(context => context.Request.Path.StartsWith("/today"), branchBuilder =>
{
branchBuilder.Run(async (HttpContext context) =>
{
await context.Response.WriteAsync($"Today is {DateTime.UtcNow.DayOfWeek}");
});
});
Path-based branching using MapWhen()
Of course, you can also use conditions that are not based on the HTTP context.
app.MapWhen(_ => DateTime.UtcNow.DayOfWeek == DayOfWeek.Friday, (IApplicationBuilder branchBuilder) =>
{
branchBuilder.Run(async (HttpContext context) =>
{
await context.Response.WriteAsync("Happy Weekend!");
});
});
To summarize the use of Map
and its variant MapWhen
should be used if several independent pipelines with different behaviors are required. And further, you should go for MapWhen
instead of Map
if you need to branch the pipeline based on the state of the HttpContext
and not just the request path.
Use & UseWhen extension method
The Use
& UseWhen
methods are the Swiss army knifes among the extension methods we've looked at. They are quite versatile as they allow reading an incoming HTTP request, generating a response, or passing it to subsequent middleware components.
// Adds a middleware delegate defined in-line to the application’s
// request pipeline
public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, RequestDelegate, Task> middleware)
Signature Use() extension method
The Use
extension expects a delegate of type Func<HttpContext, RequestDelegate, Task>
. This delegate encapsulates a method that takes two parameters HttpContext
& RequestDelegate
and returns a Task
. Therefore, it can be executed asynchronously. The RequestDelegate
represents the subsequent middleware components in the pipeline. The following example should make this more tangible.
var builder = WebApplication.CreateBuilder(args)
var app = builder.Build();
// First middleware
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Middleware1: Incoming\n");
await next.Invoke(context);
await context.Response.WriteAsync("Middleware1: Outgoing\n");
});
// Second middleware
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Middleware2: Incoming\n");
await next.Invoke(context);
await context.Response.WriteAsync("Middleware2: Outgoing\n");
});
// Terminal middleware
app.Run(async context =>
{
await context.Response.WriteAsync("Terminal middleware\n");
});
app.Run();
The first middleware writes Middleware1: Incoming
to the HTTP context. It then passes the execution control to the subsequent middleware by asynchronously invoking RequestDelegate
and passing the HTTP context (await next.Invoke(context)
).
The second middleware does the same and passes execution control onto the terminating middleware, which short-circuits the pipeline. Then, the flow reverses, and control is given back to the second middleware and finally to the first one.
❯ curl https://localhost:7014
Middleware1: Incoming
Middleware2: Incoming
Terminal middleware
Middleware2: Outgoing
Middleware1: Outgoing
Invocation
☝🏼Microsoft.AspNetCore.Builder.UseExtensions
contains an overloadpublic static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
. This overload expects aFunc<Task>
delegate instead ofRequestDelegate
. For enhanced performance, Microsoft recommends using theRequestDelegate
version. So instead ofawait next.Invoke()
useawait next.Invoke(context)
!
Similar to MapWhen
, UseWhen
allows the conditional use of middleware based on a predicate.
// Conditionally creates a branch in the request pipeline that is
// rejoined to the main pipeline
public static IApplicationBuilder UseWhen(this IApplicationBuilder app, Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration)
In the following example, the HTTP logging middleware is only used for requests to the path /images
. However, the custom header X-Today-Is
is added for every request, regardless of the path used.
The example makes clear, that subsequent middleware is always getting called when using UseWhen
independent of the predicate outcome. This contrasts MapWhen
where the ApplicationBuilder
spawns its own pipeline.
app.UseWhen(context => context.Request.Path.StartsWithSegments("/images"), applicationBuilder =>
{
applicationBuilder.UseHttpLogging();
});
// Gets called regardless of predicate of UseWhen()
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Today-Is", DateTime.UtcNow.DayOfWeek.ToString());
await next.Invoke(context);
});
Conclusion
In general, you should only use the inline version if the required functionality is very basic, as they can become difficult to read, debug and test. The next article will talk about writing convention-based components.
Thanks for reading! Happy hacking 👨🏼💻
Further reading
