Writing inline ASP.NET Core middleware using lambda expressions - part 2

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 overload public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware). This overload expects a Func<Task> delegate instead of RequestDelegate. For enhanced performance, Microsoft recommends using the RequestDelegate version. So instead of await next.Invoke() use await 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 ApplicationBuilderspawns 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

Write custom ASP.NET Core middleware
Learn how to write custom ASP.NET Core middleware.
Introduction to ASP.NET Core middleware - part 1
This article explains the high-level concepts of an ASP.NET Core middleware and is the first in a series of three articles.
Writing inline ASP.NET Core middleware using lambda expressions - part 2
This article is part of a series about ASP.NET Core middleware and explains how inline-middleware can be written using lambda expressions
Writing convention-based ASP.NET Core middleware - part 3
This article is part of a series that explains how to develop convention-based middleware components with ASP.NET Core
Writing factory-based ASP.NET Core middleware - part 4
This article is part of a series that explains how to develop factory-based ASP.NET Core middleware
Testing ASP.NET Core middleware - part 5
Introduction This is the last in a series of posts that explains how ASP.NET Core middleware components can be written. 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