I am fascinated by Middleware Stacks. Middlewares help us to customize a function invocation (typically a request handler) with additional behavior before and after the invocation of function. Popular examples are authentication, logging or exception handling middlewares. Often they handle non-functional requirements (like logging, security, ...).
As a .NET developer I use daily the prominent middleware stack in ASP.NET Core. While beautifully abstracted and designed, there is however one big caveat: It is bound to HTTP request/response. Due to that limitation I built violetgrass/middleware.
In this article I try to discuss the various elements of a ASP.NET Core inspired middleware stack implemented in pure .NET.
The delegate
A middleware is a stackable function. The function definition is called the MiddlewareDelegate
(ASP.NET Core: RequestDelegate
).
public delegate Task MiddlewareDelegate<TContext>(TContext context) where TContext : Context;
The function expose the following characteristics:
- Is is asynchronous, since most likely, some part of the stack needs to access slowlier resources (e.g. a file system).
- It receives an invocation context (essentially the input and output of the function). (ASP.NET Core:
HttpContext
). - It is a call without a return value.
- It is stateless on its own (it is a function not an interface for a class).
A simple message handler can be represented in the MiddlewareDelegate
.
async Task HandleAsync(Context context)
{
var message = GetMessageFromContext(context);
Console.WriteLine(message)
}
MiddlewareDelegate<Context> messageHandler = HandleAsync;
var message = GetMessage();
await messageHandler(message); // process the message.
A middleware however, is not only the final handler, but essentially everything in the middle between the dispatching invoker and the terminal handler. In simple cases, this could be added programmatically.
async Task HandleAsync(Context context) { /* see above */ }
async Task LogAsync(Context context)
{
Log.Write("Before");
await HandleAsync(context);
Log.Write("After");
}
async Task CatchExceptionAsync(Context context)
{
try
{
await LogAsync(context);
}
catch { /* ... */ }
}
MiddlewareDelegate<Context> messageHandler = CatchExceptionAsync;
var message = GetMessage();
await messageHandler(message); // try-catch, log and process the message.
This method of coding exposes some issues
- The composition of the functions influences the actual code of the functions.
- There is not methodology to integrate third parties or have out-of-the-box functionality.
The building of the middleware (stack)
The purpose of using a middleware framework is to enable second and third party integrations into an efficient invocation stack. This includes all attributes of regular method stacking (like controlling the code before and after the invocation and handlings exceptions).
In a first step, the invocation of the next wrapped function needs to be parameterized.
async Task LogAsync(MiddlewareDelegate<Context> next, Context context)
{
Log.Write("Before");
await next(context);
Log.Write("After");
}
However, that would imply that the delegate is recursive and would differ between middleware (which need next
) and the terminal handler (which does not need next
). Closures to rescue which can bind additional variables not represented as function parameters. Closures need to be created by another function which holds the "closured" scope ...
MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
{
return async (Context context) => // = MiddlewareDelegate<Context>
{
Log.Write("Before");
await next(context);
Log.Write("After");
};
}
// or in modern C# with local functions instead of lambdas.
MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
{
return LogAsync;
async Task LogAsync(Context context)
{
Log.Write("Before");
await next(context);
Log.Write("After");
}
}
// or if you like expression bodied member
MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
=> async context => { Log.Write("Before"); await next(context); Log.Write("After"); };
// or currying and first class functions
Func<MiddlewareDelegate<Context>, MiddlewareDelegate<Context>> LogFactory = next => async context => { Log.Write("Before"); await next(context); Log.Write("After"); };
This function which builds the middleware is called a middleware factory (Func<MiddlewareDelegate<TContext>, MiddlewareDelegate<TContext>>
). It is a function which receives the next
element in the middleware stack to built and emits a function which represent the current middleware and all middleware later in the stack.
You can now write ..
var messageHandlerStep3 = HandleAsync;
var messageHandlerStep2 = LogFactory(messageHandlerStep3);
var messageHandlerStep1 = CatchExceptionFactory(messageHandlerStep2);
var message = GetMessage();
await messageHandlerStep1(message); // try-catch, log and process the message.
So now let us build a fancy builder infrastructure for it.
A IMiddlewareBuilder<TContext>
(ASP.NET Core: IApplicationBuilder
) collects a set of middleware factories.
public IMiddlewareBuilder<TContext> Use(Func<MiddlewareDelegate<TContext>, MiddlewareDelegate<TContext>> middlewareFactory)
{
_factories.Add(middlewareFactory);
return this;
}
public MiddlewareDelegate<TContext> Build()
{
_factories.Reverse();
MiddlewareDelegate<TContext> current = context => Task.CompletedTask; // safeguard
foreach (var middlewareFactory in _factories)
{
current = middlewareFactory(current);
}
return current;
}
Note: Ignoring all the interface definitions and additional features beyond middleware, this above is the complete business logic of violetgrass/middleware.
The above Build method iterate each factory (in reverse order) and throw the built middleware function of the current factory as an input to the factory method of the next layer. An Build invocation might look like that ...
var messageHandler = new MiddlewareBuilder<Context>()
.Use(CatchExceptionFactory)
.Use(LogFactory)
.Use(next => HandleAsync) // no use of next
.Build();
var message = GetMessage();
await messageHandler(message); // try-catch, log and process the message.
Configuring the Middleware
With the use of C# extension methods, first and third party logic can be added to the MiddlewareBuilder
. The builder extension methods can be influenced by parameters, Dependency Injection and other sources of information.
Also, helper methods can be added to simplify the creation of middleware.
// Typical third part integration
public static class MiddlewareBuilderExtensions
{
public static IMiddlewareBuilder<TContext> UseLog(this IMiddlewareBuilder<TContext> self, bool verboseLogging = false)
{
// this block is run when the builder (extension) methods are invoked (one-time).
// .. allows to perform configuration and collect metadata (e.g. via parameters or DI)
return self.Use(LogFactory);
MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
{
// this block is run when the middleware stack is actually build (one-time).
// .. the shape of the middleware is known at this moment
// .. has closure of configuration
return LogAsync;
async Task LogAsync(Context context)
{
// this block runs when the middleware stack is invoked (each-request)
// .. has closure of configuration
// .. has closure of shape
// as a middleware it encapsulates the next from the building time
if (verboseLogging) { Log.Write("Before"); }
await next(context);
if (verboseLogging) { Log.Write("After"); }
}
}
}
}
// simple helper functions
public static partial class IMiddlewareBuilderExtensions
{
public static IMiddlewareBuilder<TContext> Use<TContext>(this IMiddlewareBuilder<TContext> self, MiddlewareDelegate<TContext> middleware) where TContext : Context
=> self.Use(next => async context => { await middleware(context); await next(context); });
}
var messageHandler = new MiddlewareBuilder<Context>()
.UseExceptionHandling()
.UseLog(verbose: true)
.Use(HandleAsync)
.Build();
var message = GetMessage();
await messageHandler(message); // try-catch, log and process the message.
Conclusions
This article hopefully gave an introduction into how the violetgrass/middleware and ASP.NET Core middleware stack work. While sometimes it looks over-engineered, it is actually quite simple and a powerful extensibility framework.
There is more to typically middleware scenarios like dispatching and endpoint routing, however, this is material for other articles.