Introduction

Middleware is a concept which has been around for years. In the .NET world it first started to become a concept during the OWIN pipeline processing. This was the pre-cursor to the middleware we know and love in .NET 6+ (and .NET Core) however only recently it has become a concept of middleware for Azure Functions.

Since the concept of Isolated hosting model of Azure Functions it becomes a lot closer to ASP.NET hosting. It runs from a HostBuilder and allows for configuration settings, service registrations and middleware chaining.

A basic usage of the middleware is the same as ASP.NET Core middleware.

public class ExceptionLoggingMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            var log = context.GetLogger<ExceptionLoggingMiddleware>();
            log.LogWarning(ex, string.Empty);
        }
    }
}

As you can see from above it implements a specific functions middleware interface. This allows for construction of the Invoke method which is the entry point. As per ASP.NET Core middleware it gets passed the context and the next delegate. In the functions world we have the FunctionContext and FunctionExecutionDelegate. This is specifically not http related as the middleware runs out of the box agnostic of trigger type.

Conditional Usage

So we have middleware which applies to any trigger type such as the exception logging middleware above, but other concepts are specific to the type of the trigger the function is based off.

In the ASP.NET Core world we have the concept of UseWhen and using the HttpContext to determine when the middleware is applied, such as specific route, specific claim etc. The Azure Functions gives us something similar to work with.

The FunctionContext which gets passed into the Invoke method gives access to details about the function which has triggered the request. This can be HttpTrigger, TimerTrigger anything. Due to this what is available in the context will be dependant on which trigger has been actioned.

Azure Functions also has a UseWhen construct when it comes to registering middleware with the host. You can either use the manual registration or the generic registration but either way it requires a predicate.

app.UseWhen<SampleHttpMiddleware>(context => true);

So we have a http based middleware to register but the functions app contains many, how do we get it to only work for http triggers?

This is where we start to interrogate the function context.

context.FunctionDefinition.InputBindings.Values.First(a => a.Type.EndsWith("Trigger")).Type == "httpTrigger";

The only way to do this that I found was to look at the input binding definitions on the function definition provided. With this we can look for all the items which are a trigger. Azure functions can only have 1 trigger attribute specfied so we can look for that. We can then look at the type value and compare it to what we are looking for. In this case it’s the httpTrigger.

As it’s a delegete we can pass a definition into the registration. To do this we specify the condition as a static method and we use that to register.

    public static bool IsHttp(FunctionContext context) 
        => context.FunctionDefinition.InputBindings.Values.First(a => a.Type.EndsWith("Trigger")).Type == "httpTrigger";

And the registration gets updated to …

app.UseWhen<SampleHttpMiddleware>(MiddlewarePredicates.IsHttp);

Remove The Human Factor

The one issue with the above is it requires the developer to remember to add in the correct delegate for the registration. So how can we resolve this and make it easier for the developers?

We need to make the predicate part easier to work with and not to repeat ourselves. In this implementation it only looks at the type of trigger to keep it inline with the above. It could be extended as required but for this example we’ll keep it simple.

To start off we need a way to determine the predicate. This needs to be driven by the type of trigger.

public static bool IsTriggeredBy(this FunctionContext context, string triggerType)
        => context.FunctionDefinition.InputBindings.Values.First(a => a.Type.EndsWith("Trigger")).Type == triggerType;

As we can see this condition is based off the same predicate as above. This is called with the value “httpTrigger” when executed.

How do we use it?

We create a base middleware type which can then be derived per implementation.

public abstract class TriggerMiddlewareBase : IFunctionsWorkerMiddleware
{
    public abstract string TriggerType { get; }

    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        // always run the next middleware if not applicable to the current trigger type.
        await (context.IsTriggeredBy(TriggerType) ? InnerInvoke(context, next) : next(context));
    }

    protected abstract Task InnerInvoke(FunctionContext context, FunctionExecutionDelegate next);
}

We have the ability to register any type derived from this and then use the simple use registration. This makes life easier to register the implementation.

A simple implementation would look like …

public class AddHeaderMiddleware : TriggerMiddlewareBase
{
    public override string TriggerType => "httpTrigger";

    protected override async Task InnerInvoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        await next(context);

        var responseData = context.GetHttpResponseData();
        responseData!.Headers.Add("x-my-header", Guid.NewGuid().ToString());
    }
}

We have the ability to specify that the applicable TriggerType is “httpTrigger”. We then have the ability to implement the InnerInvoke method to determine the specific processing required, whether it’s before the call to the next delegate or after, to process http based functions.

This can then be registered …

app.UseMiddleware<AddHeaderMiddleware>();

But How Do We Test?

This is where, in my opinion, Azure Functions still aren’t quite ready for prime time, or at least the isolate process model isn’t. It’s really hard to setup test constructs for FunctionContext. There is no provided integration concept yet, similar to WebApplicationFactory<> in ASP.NET Core, to run integration tests. It’s all a bit of a mess tbh.

Conclusion

In this post we have looked at a brief introduction to middleware, how to implement conditional middleware in the Azure Functions Isolated Process model and how to register the middleware.

Full examples of this code can be found on GitHub - https://github.com/WestDiscGolf/AzFunctionsPlayground/tree/main/AndMiddleware/src/AndMiddleware

Any questions/comments then please contact me on Twitter @WestDiscGolf