Runtime binding with Azure Functions

Serverless functions rarely work in isolation. Functions are often paired with other services like a database, a storage service, or perhaps even an email notification service. Azure Functions makes it easy to connect with these services by offering bindings that give you an out-of-the-box connection to an array of cloud services. You can achieve a lot using the standard configurations, but what if you need go beyond the defaults?

Suppose you want to:

  • Change a storage location depending on an incoming IP address?
  • Switch an email recipient based on a dev vs. production environment?
  • Write data to a different message queue based on a date range?

These are just a few examples of the type of adjustments which may be difficult using a binding's default settings. This article demonstrates how to configure a C# Azure Functions binding at run time to take full control over a binding's configuration.

td;dr

To configure a binding at runtime:

  1. Declare the binding parameter as a Binder or IBinder instance.
  2. Create an instance of the binding attribute.
  3. Customize the binding attribute as necessary.
  4. Either use Bind or BindAsync to bind the attribute to a cloud service.
  5. Use the binding

A working example is available on GitHub: azure-functions-runtime-binding.

Default binding

Before diving into the details of how runtime binding works, first consider the default scenario. The following code example implements an HTTP-triggered function that saves a message to an Azure Storage blob container.

[FunctionName("SaveDefault")]
public static IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
    HttpRequest req,

    [Blob("messages/{sys.randguid}.txt", FileAccess.Write)] out string blob,
    ILogger log)
{
    string message = req.Query["message"];
    blob = $"Default binding: {message}";

    return new OkResult();
}

For the binding to work in this context, the blob parameter is set up in the following ways:

  • The Blob attribute is declared to write data mapped to a blob container named messages.
  • The {sys.randguid} replacement token ensures the new blob is given a unique name.
  • The parameter is declared as a string.
  • The parameter is declared as an out parameter. Using an out parameter gives the binding a chance to take the primitive string's value and use it to write out to the storage container elsewhere in the binding's logic.

This setup works great for simple scenarios. However, using this approach makes it difficult to make changes to the binding. In some cases you may want to:

  • Change the connection string.
  • Calculate the blob name and/or the container's name.

    • You do have a few replacement tokens and route parameters available to influence the blob path, but some needs are more complex.
  • Dynamically change the file access type. Perhaps in some instances you want to create the attribute with Write or ReadWrite access levels.

To achieve more flexible binding customization, you can implement runtime binding.

Runtime binding

The final result of the following function is the same as implemented for the default scenario. The difference in this instance is that the binding is imperatively created, giving you the chance to affect a binding's behavior.

[FunctionName("SaveCustom")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
    HttpRequest req,
    IBinder binder,
    ILogger log)
{
    string message = req.Query["message"];

    var attribute = new BlobAttribute("messages/{sys.randguid}.txt", FileAccess.Write);
    attribute.Connection = "AzureStorageConnectionString";

    using(var blob = await binder.BindAsync<TextWriter>(attribute))
    {
        blob.Write($"Runtime binding: {message}");
    }

    return new OkResult();
}

The binding is declared as an IBinder type, which gives you the chance to determine the binding configuration.

In this scenario:

  • An instance of BlobAttribute is created by passing the same parameters to the constructor as shown in the default example.

    • Here you could decide to use custom logic to determine the blob container name or generate a custom blob name.
  • The Connection property is set.

    • Here you could swap out an app setting name depending on the environment or perhaps input to the function.
  • The blob binding is created by calling binder.BindAsync since the TextWriter class is used to write to the blob container.
  • Calling blob.Write is what eventually persists the data to the blob container.

NOTE: Instead of typing the binding as a string (as shown in the default scenario), the binding is created as a TextWriter. The TextWriter class is used because a string instance has no knowledge of how to write back to the blob container. The TextWriter is configured to know about the blob container through the call of binder.BindAsync.

Code sample

The sample code is available on GitHub: azure-functions-runtime-binding.

Summary

The built-in Azure Functions bindings give you rich access to a number of different cloud services. While the default behavior may work well in many cases, sometime you need the chance to customize a binding's configuration.

You can customize a binding at runtime via the following steps:

  1. Declare the binding parameter as a Binder or IBinder instance.
  2. Create an instance of the binding attribute.
  3. Customize the binding attribute as necessary.
  4. Either use Bind or BindAsync to bind the attribute to a cloud service.
  5. Use the binding