Posts Dynamic Binding in Azure Functions
Post
Cancel

Dynamic Binding in Azure Functions

Azure Functions Binding Binding Attributes in a C# flavored Azure Function offers great functionality out of the box. Most of the time these declarative bindings are enough. The deal of having a declarative implementation arises when you want more control on the binding or when and how it is going to be triggered. Digging under the hood, these Binding Attributes can be replaced with IBinder or Binder instance which allows for imperative dynamic binding.

The Declarative Catch (Attribute-based Binding)

1
2
3
4
5
6
7
8
9
[FunctionName("Bindings")]
[StorageAccount("AzureWebJobsStorage")]
public static async Task Run(
    [BlobTrigger("container/{blobName}.{ext}")] Stream input,
    [Blob("pass/{blobName}.{ext}", FileAccess.Write)] Stream outputPass,
    [Blob("fail/{blobName}.{ext}", FileAccess.Write)] Stream outputFail,
    string blobName,
    string ext)
{ ... }

The code above contains two (2) Blob Bindings, both directions are set to output (e.g. FileAccess.Write). The Function is also triggered with a Blob input. The first binding will serve as the output destination when the input Blob is successfully processed and the second binding will serve as the output destination when the processing fails on the input Blob.

  • Uploading a Passing blob Attribute Binding Result Attribute Binding Pass Attribute Binding Fail

From both tests we can confirm that both transfers the Blob in the correct directory, but an empty Blob is created on Failing directory for the Passing Test and vice versa. The culprit is the declarative binding which happens at compile-time, even if the supposed output is directed at one of the Streams.

The Imperative Solution (Dynamic Binding)

The solution to the dilemma is to move the binding execution from compile-time to run-time. It can be achieved by “Injecting” the IBinder or Binder instance to the Function. The interface provides one (1) method called BindAsync which accepts an Attribute parameter.

1
2
3
4
5
6
public interface IBinder
{
    Task<T> BindAsync<T>(
        Attribute attribute, 
        CancellationToken cancellationToken = default(CancellationToken));
}
  1. “Inject” the IBinder or Binder instance to the Function’s parameter. The binding parameters can now be replaced with a single IBinder or Binder instance.
    1
    2
    3
    4
    5
    6
    7
    8
    
    [FunctionName("Bindings")]
    [StorageAccount("AzureWebJobsStorage")]
    public static async Task Run(
     [BlobTrigger("container/{blobName}.{ext}")] Stream input,
     IBinder binder,
     string blobName,
     string ext)
    { ... }
    
  2. Create the Attribute Object Instead of declaring the arguments in the parameter attribute. An Attribute object must now be created, it can also be an Attribute[]. For the Blob Binding, a BlobAttribute must be initialized with the Path and Direction. Optionally, the StorageAccountAttribute can be set if it is part of the binding option (e.g. Connection = "AzureWebJobsStorage").
    1
    2
    3
    4
    5
    
    var attributes = new Attribute[]
    {
      new BlobAttribute(path, FileAccess.Write),
     // new StorageAccountAttribute(FunctionConfig.Blob)
    };
    

    The following Attributes for common binding are:

  3. Invoke the BindAsync method Pass the Attributes and set the Type. The Type is specialized to the binding, this is usually the type on the parameter when an Attribute-based Binding is used. The Blob Binding prefers a Stream or CloudBlockBlob type for better memory utilization.
    1
    2
    3
    4
    
    using (var output = await binder.BindAsync<Stream>(attributes))
    {
     // Transfer logic
    }
    

    Note: From the Microsoft Docs, the Binder instance must be properly disposed by wrapping in a Using Block. In C# 8, the Using Blocks can now be an Expression for brevity.

Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[FunctionName("Bindings")]
public static async Task Run(
    [BlobTrigger("container/{blobName}.{ext}")] Stream input,
    IBinder binder,
    string blobName,
    string ext)
{
    var path = string.Empty;
    var storageAccount = "AzureWebJobsStorage";

    try
    {
        // Process input blob
        path = $"pass/{blobName}.{ext}";
    }
    catch(Exception ex)
    {
        path = $"fail/{blobName}.{ext}";
    }

    var attributes = SetAttributes(path, storageAccount);
    await TransferBlobAsync(input, attributes);
}

private static Attribute[] SetAttribute(string path, string storageAccount)
{
    var attributes = new Attribute[]
    {
        new BlobAttribute(path, FileAccess.Write),
        new StorageAccountAttribute(storageAccount)
    };
}

private static async Task TransferBlobAsync(Stream input, Attribute[] attributes)
{
    using (var output = await binder.BindAsync<Stream>(attributes))
    {
        // Transfer logic
    }
}

Upon triggering the Function again, the tests showed that the lingering empty Blobs does not get created anymore. Since the binds are dynamically made on run-time.

  • Uploading a Passing blob Dynamic Binding Result Dyanmic Binding Pass Dynamic Binding Fail

Dynamic Binding offers flexibility and control on Azure Functions Bindings as well as minimizing parameter clutter. A lot more applications can take advantage of this technique versus the traditional Attribute-based Binding where conditional operations must be performed on the Bindings.

Search Results