I created a simple middleware pipeline builder that I want to use for my frameworks. It is similar to how ASP.NET-Core middleware works and uses the same conventions:
- A public constructor with a parameter of type
RequestDelegate
.- A public method named
Invoke
orInvokeAsync
. This method must:
- Return a
Task
.- Accept a first parameter of type
HttpContext
.
The only difference is that I use a generic delegate
for next
that I renamed:
public delegate Task RequestCallback<TContext>(TContext context);
API
The whole thing is quite simple. It uses a Stack
to store registered middleware types (currently without constructor parameters) and so that I can process them in the reverse order. I need this to inject the last middleware's Invoke
as next of the one that is going to be created aftewards and so on. The last one, or the first added one, is the starting point.
I use Autofac
to resolve dependencies.
public class PipelineBuilder
{
private readonly Stack<Type> _middlewareTypes = new Stack<Type>();
private readonly ILifetimeScope _lifetimeScope;
public PipelineBuilder(ILifetimeScope lifetimeScope)
{
_lifetimeScope = lifetimeScope;
}
public PipelineBuilder Add<T>()
{
_middlewareTypes.Push(typeof(T));
return this;
}
public RequestCallback<TContext> Build<TContext>()
{
var previous = default(object);
while (_middlewareTypes.Any())
{
var middlewareType = _middlewareTypes.Pop();
var next = CreateNext<TContext>(previous);
previous = _lifetimeScope.Resolve(middlewareType, new TypedParameter(typeof(RequestCallback<TContext>), next));
}
return CreateNext<TContext>(previous);
}
// Using this helper to "catch" the "previous" middleware before it goes out of scope and is overwritten by the loop.
private RequestCallback<TContext> CreateNext<TContext>(object previous)
{
// Doing it here to reflection next time.
var nextInvoke = previous?.GetType().GetMethod("Invoke") ?? previous?.GetType().GetMethod("InvokeAsync");
return (RequestCallback<TContext>)(context =>
{
if (previous is null)
{
return Task.CompletedTask;
}
else
{
return (Task)nextInvoke.Invoke(previous, new object[] { context });
}
});
}
}
Demo
I tested it with four dummy middlewares that build a message. Here's the complete code:
void Main()
{
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<M1>();
containerBuilder.RegisterType<M2>();
containerBuilder.RegisterType<M3>();
containerBuilder.RegisterType<M4>();
var container = containerBuilder.Build();
var lifetimeScope = container.BeginLifetimeScope();
var pipelineBuilder = new PipelineBuilder(lifetimeScope);
pipelineBuilder.Add<M1>();
pipelineBuilder.Add<M2>();
pipelineBuilder.Add<M3>();
pipelineBuilder.Add<M4>();
var next = pipelineBuilder.Build<TestContext>();
var context = new TestContext { Message = "" };
next(context);
context.Message.Dump(); // --> "Spongebob Squarepants"
}
public class TestContext
{
public string Message { get; set; }
}
class M1
{
private readonly RequestCallback<TestContext> _next;
public M1(RequestCallback<TestContext> next) => _next = next;
public Task Invoke(TestContext context)
{
context.Message = "Sponge";
return _next(context);
}
}
class M2
{
private readonly RequestCallback<TestContext> _next;
public M2(RequestCallback<TestContext> next) => _next = next;
public Task Invoke(TestContext context)
{
context.Message += "bob";
return _next(context);
}
}
class M3
{
private readonly RequestCallback<TestContext> _next;
public M3(RequestCallback<TestContext> next) => _next = next;
public Task Invoke(TestContext context)
{
context.Message += " Square";
return _next(context);
}
}
class M4
{
private readonly RequestCallback<TestContext> _next;
public M4(RequestCallback<TestContext> next) => _next = next;
public Task Invoke(TestContext context)
{
context.Message += "pants";
return _next(context);
}
}
Questions
What do you think? Can we make it better? Please ignore the missing null-checks... it's an early proof-of-concept.
InvokeAsyc
would be more conventional and I didn't cheat by looking at the original source-code :-P so I actually don't know which one of them asp.net-core favors. \$\endgroup\$