Last week one of my colleagues had a question: Is there a performance penalty when you add multiple Azure Blob storage bindings in an Azure Function? Or is the connection only established when you access one of those blobs? Answering this grew into a way to have Dynamic output bindings in Azure Functions.

This post elaborates on Using Triggers & Bindings in Azure Functions V2.

First try

The idea was simple:

  • Create a Blob-triggered Azure Function.
  • Add two Azure Blob storage output bindings.
  • Have some logic determine which of the bindings to actually use.
  • Check if only the accessed Blob is there.

Here’s what the first try looked like:

[FunctionName("DynamicOutputBindings")]
 public static async Task Run(
     [BlobTrigger("upload/{name}", Connection = "scs")] Stream uploadedBlob,
     [Blob("copied-scenario1/{name}", FileAccess.Write)] Stream scenarioOneStream,
     [Blob("copied-scenario2/{name}", FileAccess.Write)] Stream scenarioTwoStream,
     string name, ILogger log)
 {
     if (SomeValue())
     {
         await uploadedBlob.CopyToAsync(scenarioOneStream);
     }
     else
     {
         await uploadedBlob.CopyToAsync(scenarioTwoStream);
     }
 }

 private static bool SomeValue()
 {
     return (DateTime.Now.Millisecond % 2) == 0;
 }
Dynamic bindings
Behold: the result of our first try!

The initial idea was that only the Blob that was actually accessed would be created. But since it’s default behavior for an output binding to expect the binding to be necessary, it is created as soon as the Function is executed. So the fact that both containers hold a blob with the same name is expected.

The details are in the Size column. Although both containers contain the blob, only one contains the contents. This matches the logic of the Function: based on some very complex logic, only one of the two Blob bindings is copied to.

Try again

To be able to actually have a dynamic binding, a binding that only creates a Blob when it is actually needed, we use the Binder class (the terribly complex SomeValue method was left unchanged ?) to dynamically bind against one of either Blobs.

[FunctionName("DynamicOutputBindings")]
public static async Task Run(
    [BlobTrigger("upload/{name}", Connection = "scs")] Stream uploadedBlob,
    Binder binder,
    string name, ILogger log)
{
    BlobAttribute blobAttribute;
    if (SomeValue())
    {
        blobAttribute = new BlobAttribute("copied-scenario1/{name}", FileAccess.Write);
    }
    else
    {
        blobAttribute = new BlobAttribute("copied-scenario2/{name}", FileAccess.Write);
    }

    using (var output = await binder.BindAsync<Stream>(blobAttribute))
    {
        await uploadedBlob.CopyToAsync(output);
    }
}

This results in only one of the containers having a Blob matching the uploaded one; the container that is actually needed based on the logic inside of the Azure Function.

Hope this helps.