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.
By accessing the RequestDelegate
parameter, your middleware can read from the HTTP context, which carries all request details.
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
.
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.
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.
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.
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
.
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.
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.
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
.
In the example below branching only occurs, if an HTTP request contains the header X-Custom-Header
.
A condensed version could look like this
An alternative version to the path-based branching could look as follows
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.
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.
☝🏼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 👨🏼💻