Ever wanted to run some heavier initialization when your function app starts, but could not do it in the Startup class? Well, in this article I will walk you through creating a simple Azure Functions trigger that fires only once, exactly when you need it …🥁… when the runtime starts.
If you'd rather see the code, and skip the explanation, you can go straight to Github and check it out. Or if you prefer to just use it, and trust me that it works, you can go to Nuget and take it from there.
I went through the trouble of creating a proper library and put it on Nuget, because I am using it in one of my work projects, and I believe it is not such an uncommon scenario. Even if you do not have a use case for this, you might still want to go through the article since it is a great starting point for writing your own custom triggers and bindings for Azure Functions. It has all the pieces that you need to put together for such and endeavor with little extra code.
As for my use case, one of the solutions I am working on has a backend plane made of a mix of web APIs and function apps. In order to send messages between them, we use a thin wrapper on top of the Azure Service Bus. We have a single topic (one per tenant) in the Azure Service Bus namespace where all the publishers put their messages, and whoever is interested in a particular message type, can subscribe to it. Publishing messages is easy, since the topic is created, and its name is known in advance. Consuming the messages is a bit more complicated, since different services are interested in different types of messages, and each type of message has its own handler.
In the web APIs we solved this by having some helper code run on startup that goes through all the relevant assemblies and gathers the message type handlers that the service defines, and then creates the corresponding subscriptions on the topic.
In the function apps we could have had a function per message type. However, this was not acceptable for multiple reasons. First, the Azure Service Bus binding does not support managed identity yet. Second, the binding does not create the subscriptions if they do not exist, so that means that for every new message type, we would have had to create all the required subscriptions in advance. And last, why have some different approach, when the solution for web APIs works so great. The only problem was that we could not run the helper code in the Startup class, since the Startup class is meant for only setup and registration of services for DI.
I figured out that I could run the helper code in a function that is triggered at startup. Without knowing how the bindings and triggers work, I knew it was possible, since the Timer trigger has the RunOnStartup attribute which does exactly that.
Let's start with the trigger, which is the attribute the marks the function that should run at startup. You can see the code in the following listing:
Nothing to see here, move along!
Since the trigger is to be used on a parameter of a function, let's see the parameter type next. A trigger can support multiple parameter types, see the QueueTrigger for example, but it would be overkill for this particular case:
I added some common sense properties of the function app to the StartupInfo that can be useful in the triggered function. In my case, I use the name and the version, for example, to namespace the subscriptions, so that multiple services can subscribe to the same message types.
And with that we are done with the visible parts of the trigger. Next, let's see how to register the trigger with the runtime.
When the runtime starts, it scans all the linked assemblies for the WebJobsStartupAttribute assembly attribute which points to a class that implements the IWebJobsStartup interface. Once it gathers all these implementations, it calls Configure on each.
This is exactly how the Startup class is discovered as well, since FunctionsStartupAttribute actually inherits from WebJobsStartupAttribute and FunctionsStartup is an abstract class that implements IWebJobsStartup.
Here is the relevant code for how I register my extension:
The only thing that I do in the Configure implementation is call the AddExtension extension method of the IWebJobsBuilder. AddExtension expects an implementation of the IExtensionConfigProvider interface, which you can see in the following listing:
In the IExtensionConfigProvider I can already make use of DI, so in the above implementation I get some IOptions. These should be registered in the Startup class.
The really important thing that happens here is actually registering the StartupTriggerAttribute as a binding by calling the AddBindingRule and then connecting the binding with an implementation of the ITriggerBindingProvider by calling the BindToTrigger method. If you are writing a custom input or output binding you would be calling BindToInput or BindToOutput instead.
The ITriggerBindingProvider implementation is where I create the actual binding instance. The TryCreateAsync method of the provider is called for all the parameters of the function, but it returns a binding only for the ones that have the binding attribute applied and have a type supported by the binding. Since I only have to deal with parameters of type StartupInfo, the code is really simple:
The binding is an implementation of ITriggerBinding and it is where the heavy lifting is done. The CreateListenerAsync is called by the runtime to create a listener for the events that trigger the function. The BindingDataContract is how the binding exposes binding parameters that can be used in the binding expressions of other parameter attributes, and BindAsync is called by the runtime when it needs to bind to a parameter for a function invocation. The input parameter value might not be of the type expected by the trigger parameter, so this is where any conversion should happen. Finally, this method wraps up the parameter in an ITriggerData to be used by the runtime for the function invocation. Following is the binding implementation:
And finally we got to the listener which implements the IListener interface and is the place where listening for the trigger event source happens. In my case, the only event I am interested in, is the one happening when StartAsync is called, so I immediately invoke the function, no need to wait for any other event to happen. One thing to notice is that the listener uses the singleton attribute which ensures that only one instance of the listener is running regardless of the number of function app instances:
It is worth mentioning that the StartAsync method of the listener will be called when the runtime starts. But when does the runtime start? It turns out that it starts either when the function app starts, restarts, scales up or wakes up after being idle. So keep that in mind and make sure it fits with your use case.