Build your own Azure Functions Binding Extensions

Published on
Reading time
Authors

I recently wrote a post on sending Azure retirement announcements via email using the Azure Communication Services Email capabilities. At the time of writing that post the Azure Communication Services Email capabilities are still in preview which means there are still some gaps in SDKs and other supporting libraries such a binding for use with Azure Functions. Building a custom binding extension has been on my to-do list for a while, so I'm happy I found a scenario where I can try it out!

Over the years I've used the existing SendGrid binding for my email processing needs, though there have been a few changes over the past few years, such as the removal of the free tier (yes, it's back), that meant I haven't really used it for anything. As a result, when I spotted the launch of the Azure Communication Services Email offering, I decided to dig in and see if I could use it as the basis to learn how to build Azure Functions extensions.

Azure Function Custom Extensions documentation and samples

I thought this would be pretty easy to find, or there'd be a single simple template I can use. It turns out it's not that straightforward. I found there is a large range of information floating around on building extensions, some of it quite old, and none of it on the official Microsoft documentation site (aka Learn).

It turns out the definitive source of information is still the "Azure Webjobs SDK" wiki on GitHub. It's a great overview, but I actually found it really hard to visualise the extension building process due to how they mix code and text and don't use a consistent example for the code throughout. The introduction also refers you to existing extensions - a great way to learn for sure, but if you go look at some of those your head almost explodes because there is a lot in them given they are designed to support Azure services such as Service Bus!

I also mentioned that a lot of content online appears old, but after having built an extension, I can say that the age of a lot of content isn't an issue as the way you build extensions has not changed much since the introduction of the Functions runtime v2. I reached out to a former MVP who now works on the Cosmos DB team and he pointed me at a great resource he'd built so he could understand a bit more about what it takes. You can find Matias' sample on GitHub, and I think it's a good resource to review before getting started.

Extension basics

All extensions are authored as .NET class libraries using C# and targetting netstandard2.0. You can use any IDE you want to author extensions as long as you have a .NET SDK installed that supports netstandard2.0 (.NET 6 for example). You can also build using any OS you like - Linux, Mac or Windows! Let's go ahead and create a new .NET project to create a custom extension. Use the following command-line arguments to get going.

dotnet new classlib -f netstandard2.0 MyCustomExtension
dotnet add package Microsoft.Azure.WebJobs
dotnet add package Microsoft.Azure.WebJobs.Sources

Now you have a class library project, you want to create the following four classes/files:

MyCustomExtensionBindingAttribute.cs

Contains the .NET Attribute you use in your Azure Function defintion. For example, the standard Blob output binding.

MyCustomExtensionBinding.cs

Initialisation for your custom extension. Must implement the IExtensionConfigProvider interface.

MyCustomExtension.cs

Contains the code responsible for registering your custom extension with the Azure Functions host and ensuring configuration is applied.

MyCustomExtensionStartup.cs

The Startup class implements the IWebJobsStartup interface and is responsible for runtime registration of the extension using the code contained in the MyCustomExtension class mentioned above.

The above four files represent the core needed for an Azure Functions extension project. If you look at some of the production extensions such as those for Service Bus you will find a lot of other classes or files. Don't be put off by those - they are really for advanced implementations and may not apply to your use case.

It's likely that you will also need a Plain Old C# Object (POCO) to use as a model class, so go ahead and create one that matches your use case. See below for how I used the existing EmailMessage class in my implementation.

Rather than work through a synthetic example here I'd recommend checking out Azure Tips and Tricks # 247 which has a pratical example that shows how you use a POCO and the four classes / files to build a fully functional binding extension.

If you want to understand my use case and see the resulting solution (and how I tested it), read on.

Building an Azure Communication Service Email extension

Now I understood the basics I wanted to implement my own extension to send email using the Azure Communication Service. I thought a good place to start would be with the existing SendGrid binding source code on GitHub as this existing binding does mostly what I need already, but not with the service I want to use.

If you take a look at the repository, you will see there are substantially more files in this project than the sample above. That's because this extension provides more support around configuration at runtime, configuration validation and also uses an IAsyncCollector which supports writing multiple values to an output binding.

Additionally, there is an internal implementation of a ConcurrentQueue and SendGrid client cache to ensure the binding can cope with scale-out inducing load. These aren't necessary to build a binding, but at scale they may be required to avoid lost data. As the SendGrid binding used them I adopted them for the Azure Communication Service Email implementation as well.

You will also see that the SendGrid binding does not have a custom POCO, instead using the SendGrid SDK's SenGridMessage class. I wanted to use the same approach for my binding and use the EmailMessage class for the Communication Services Email SDK.

In order to implement my extension I needed to add one nuget package to my solution - the one for Azure Communication Services Email offering.

dotnet add package Azure.Communication.Email

I think the easiest step at this point is to pop open the final implementation of my custom extension and look through the project on GitHub, clone it and compile it. The folder structure also follows the one from the SendGrid extension, and it seems like many of the existing extensions use this structure, so I've kept it. You could just drop all these files into the root folder if you wanted.

Next, let's look at how we can test the resulting extension.

Testing the new extension

The official Webjobs SDK repository has unit tests for the SendGrid binding, but in my scenario I haven't used unit tests (bad me!) as this is for learning purposes only. If I was rolling this out to production, I'd be a good boy at that point and add them, I promise! 😉

So how did I test my extension? I used it in an Azure Function of course!

The result of your custom extension project is a .NET assembly (DLL) which you can copy to an Azure Function project and add as a reference. For example, I've copied the assembly to a sub-folder 'dlls' in my project and then manually added a reference to it in the Azure Functions csproject file as shown below. I picked up this idea from Jason Roberts blog post on building custom extensions (highly recommended reading as well!)

<ItemGroup>
  <Reference Include="SiliconValve.Demo.AcsMailBinding">
    <HintPath>dlls\AcsEmailBinding.dll</HintPath>
  </Reference>
</ItemGroup>

In a production scenario you would more likely package up your extensions into a nuget package and then use dotnet add package to manage.

Again, rather than reproducing a stack of code here, I'll point you at the test harness project I used, which is stored in a GitHub repository. You can fork it, clone it and run it (you'll need Visual Studio Code, the Azurite extension and the Azure Functions extension). Clearly, you'd also need a functional Azure Communication Services Email service to use as well!

At this point you can now write an Azure Function with a custom binding as follows (this one uses a HTTP Trigger to run).

[FunctionName("SimonTestFunc")]
public static async Task<IActionResult> Run(
  [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
  [CommunicationServiceEmail()] IAsyncCollector<EmailMessage> emailClient,
  ILogger log)
{
  var emailContent = new EmailContent(subject:"My Test Functions email")
                      {
                        PlainText = "Azure Functions Custom Extensions test!"
                      };

  var recipients = new EmailRecipients(to: new List<EmailAddress>()
                    {
                      new EmailAddress("not.a.real.person@no-such-domain.example")
                    });

  var emailMessage = new EmailMessage("DoNotReply@your_custom_email_subdomain.azurecomm.net", emailContent, recipients);

  await emailClient.AddAsync(emailMessage);

  return new OkObjectResult("Yes!");
}

If you run this Function with a valid configuration, you will receive an email similar to the below. Now you can use Azure Functions to send email using just Azure-hosted services!

Image of email in Outlook

Supporting Function languages other than .NET

So, great, you have an extension! What about runtime languages other than .NET?

Thankfully you don't need to fully reimplement your custom extension for each runtime language - all you need to do is build an Annotation for the language you want to support and make it available to developers in that language (say, Java).

I'm currently working through this scenario and will update this post with details when I'm done.

Here are all the links from the post above in a single place for easy use!

Happy Days 😎