OdeToCode IC Logo

How the Next Delegate Works In ASP.NET Core Middleware

Monday, September 17, 2018

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.

The Goal

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]);

The Implementation

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.


Comments
Gravatar Graham King Monday, September 17, 2018
Nice write up Scott. Interestingly the actual implementation doesn't use a linked list, but the result is the same. https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs#L80 On a side note, what does TestHttpContext look like? I can't find an example of this for unit testing. Many thanks
Gravatar Dasith Monday, September 17, 2018
Hey Scott. Good read. A middleware is a reducer or a left fold in functional terms. Therefore you can do something like this as well. https://github.com/dasiths/SimpleMediator/blob/82a0418ad55b27c11cf00ec9cc1b34261c373052/SimpleMediator/Middleware/RequestProcessor.cs#L63
Gravatar scott Tuesday, September 18, 2018
Graham, Dasith - awesome links.
David Fowler Thursday, September 20, 2018
You can use the DefaultHttpContext, no need to write a test one
Gravatar scott Friday, September 21, 2018
@David: Good point
Comments are closed.