Azure Functions 2.0 extensibility - overview
Recently I needed to dive into Azure Functions extensibility. While doing so, I've decided to put together this post. Information regarding Azure Functions extensibility are available but scattered. I wanted to systematize them in hope of making the learning curve more gentle. Let me start with following diagram.
This diagram is nowhere near completed and shows a certain perspective, but I consider it a good representation of key parts of Azure Functions extensibility model and relations between them. There are few aspects which should be discussed in more details.
Discoverability
For extension to be usable, Azure Functions runtime needs to be able to discover it. The discovery process looks for WebJobsStartupAttribute
assembly attribute, which role is to point at IWebJobsStartup
interface implementation. IWebJobsStartup
interface has a single method (Configure
), which will be called during host startup. This is the moment when an extension can be added. Simplest implementation could look like below.
[assembly: WebJobsStartup(typeof(CustomExtensionWebJobsStartup))]
public class CustomExtensionWebJobsStartup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.AddExtension<CustomExtensionConfigProvider>();
}
}
Of course usually we need more. We might want to configure some options or register additional services. In such case it's nice practice to extract all this code to an extension method.
public static class CustomExtensionWebJobsBuilderExtensions
{
public static IWebJobsBuilder AddCustomExtension(this IWebJobsBuilder builder,
Action<CustomExtensionOptions> configure)
{
// Null checks removed for brevity
...
builder.AddCustomExtension();
builder.Services.Configure(configure);
return builder;
}
public static IWebJobsBuilder AddCustomExtension(this IWebJobsBuilder builder)
{
// Null checks removed for brevity
...
builder.AddExtension<CustomExtensionExtensionConfigProvider>()
.ConfigureOptions<CustomExtensionOptions>((config, path, options) =>
{
...
});
builder.Services.AddSingleton<ICustomExtensionService, CustomExtensionService>();
return builder;
}
}
This way it remains clear, that IWebJobsStartup
responsibility is to add extension, while all necessary plumbing is separated.
[assembly: WebJobsStartup(typeof(CustomExtensionWebJobsStartup))]
public class CustomExtensionWebJobsStartup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.AddCustomExtension();
}
}
Triggers, Inputs and Outputs
After Azure Functions runtime discovers the extensions, the real fun begins. The runtime will call Initialize
method of IExtensionConfigProvider
implementation, which allows for registering bindings. This is done by adding binding rules to ExtensionConfigContext
. At the root of every rule there is an attribute. In most scenarios having two attributes is sufficient - one for input and output bindings, and one for trigger bindings.
[Extension("CustomExtension")]
internal class CustomExtensionExtensionConfigProvider : IExtensionConfigProvider
{
...
public void Initialize(ExtensionConfigContext context)
{
// Null checks removed for brevity
...
var inputOuputBindingRule = context.AddBindingRule<CustomExtensionAttribute>();
...
var triggerAttributeBindingRule = context.AddBindingRule<CustomExtensionTriggerAttribute>();
...
}
}
Input and output bindings are a not that complicated. For specific types you can use BindToInput
method, which takes either an IConverter
or a Func
which needs to be able to convert from attribute to specified type (attribute is a source of configuration here) and can take IExtensionConfigProvider
as parameter (which is important as IExtensionConfigProvider
instance is capable of taking dependencies from DI through its constructor). More generic approach is using BindToValueProvider
method. Here you provide a function which gets type as parameter and gets a chance to figure out how to provide value. There is a special case, binding to IAsyncCollector
and ICollector
(which are special "out" types in Azure Functions). For this purpose you must call BindToCollector
, but approach is similar to BindToInput
. Specific implementations will vary depending and what is being bind, but proper design of available bindings is crucial as binding are the place to deal with asynchrony. Additionally bindings (thanks to possibility of using IAsyncCollector
and IAsyncConverter
) give you an alternative way to deal with asynchrony.
Trigger bindings are different. First step is simple, you need to call BindToTrigger
with an instance of ITriggerBindingProvider
implementation as parameter. After that you only need to implement ITriggerBindingProvider
, ITriggerBinding
and IListener
(it's kind like drawing an owl). This is not a straightforward job and strongly depends on what is going to be your feed. Discussing this "in theory" wouldn't do much good, so I'm planning on writing a separated post on trigger binding in a specific context.
Some real code
I don't like doing things in vacuum. This is why when I've started learning about Azure Functions extensibility I've decided that I need some real context. Here you can find the side project I'm working on.