How does next
know how to call the next piece of middleware in the HTTP processing pipeline? I’ve been asked this question more than once when helping to write middleware components for ASP.NET Core.
I thought it might be fun to answer the question by showing the code for an implementation of IApplicationBuilder
. Keep in mind the code is meant to demonstrate how to build a middleware pipeline. There is no error handling, no optimizations, no pipeline branching features, and no service provider.
We want an app builder with a Use
method just like a real application builder, that is a Use
method that takes a Func<RequestDelegate, RequestDelegate>
. This Func<>
represents a middleware component.
When we invoke the function we have to pass in a next
delegate that represents the next piece of middleware in the pipeline. What we get back when we invoke the function is a second function that we can use to process each individual HTTP request.
The code below looks just like the code in the Configure method of a web app, although the middleware doesn’t do any real work. Instead, the components write log statements into a fake HTTP context.
app.Use(next => { return async ctx => { ctx.AddLogItem("Enter middleware 1"); await next(ctx); ctx.AddLogItem("Exit middleware 1"); }; }); app.Use(next => { return async ctx => { ctx.AddLogItem("Enter middleware 2"); await next(ctx); ctx.AddLogItem("Exit middleware 2"); }; }); app.Use(next => { return async ctx => { ctx.AddLogItem("Enter middleware 3"); await next(ctx); ctx.AddLogItem("Exit middleware 3"); }; });
If we were to look at the log created during execution of the test, we should see log entries in this order:
Enter middleware 1 Enter middleware 2 Enter middleware 3 Exit middleware 3 Exit middleware 2 Exit middleware 1
In a unit test with the above code, I expect to be able to use the app builder to build a pipeline for processing requests represented by an HttpContext
.
var pipeline = app.Build(); var request = new TestHttpContext(); pipeline(request); var log = request.GetLogItem(); Assert.Equal(6, log.Count); Assert.Equal("Enter middleware 1", log[0]); Assert.Equal("Exit middleware 1", log[5]);
Each time there is a call to app.Use
, we are going to need to keep track of the middleware component the code is adding to the pipeline. We’ll use the following class to hold the component. The class will also hold the next
pointer, which we’ll have to compute later after all the calls to Use
are finished and we know which component comes next. We’ll also store the Process
delegate, which represents the HTTP message processing function returned by the component Func
(which we can’t invoke until we know what comes next).
public class MiddlewareComponentNode { public RequestDelegate Next; public RequestDelegate Process; public Func<RequestDelegate, RequestDelegate> Component; }
In the application builder class, we only need to store a list of the component being registered with each call to Use
. Later, when building the pipeline, the ability to look forwards and backwards from a given component will prove useful, so we’ll add the components to a linked list.
public void Use(Func<RequestDelegate, RequestDelegate> component) { var node = new MiddlewareComponentNode { Component = component }; Components.AddLast(node); } LinkedList<MiddlewareComponentNode> Components = new LinkedList<MiddlewareComponentNode>();
The real magic happens in Build
. We’ll start with the last component in the pipeline and loop until we reach the first component. For each component, we have to create the next
delegate. next
will either point to the processing function for the next middleware component, or for the last component, be a function we provide that has no logic, or maybe sets the response status to 404. Once we have the next
delegate, we can invoke the component function to create the processing function itself.
public RequestDelegate Build() { var node = Components.Last; while(node != null) { node.Value.Next = GetNextFunc(node); node.Value.Process = node.Value.Component(node.Value.Next); node = node.Previous; } return Components.First.Value.Process; } private RequestDelegate GetNextFunc(LinkedListNode<MiddlewareComponentNode> node) { if(node.Next == null) { // no more middleware components left in the list return ctx => { // consider a 404 status since no other middleware processed the request ctx.Response.StatusCode = 404; return Task.CompletedTask; }; } else { return node.Next.Value.Process; } }
This has been a "Build Your Own AppBuilder" excercise. "Build you own ________" exercises like this are a fun challenge and a good way to understand how a specific piece of software works behind the scene.