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
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));
}
- “Inject” the
IBinderorBinderinstance to the Function’s parameter. The binding parameters can now be replaced with a singleIBinderorBinderinstance.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) { ... }
- Create the Attribute Object Instead of declaring the arguments in the parameter attribute. An
Attributeobject must now be created, it can also be anAttribute[]. For the Blob Binding, aBlobAttributemust be initialized with the Path and Direction. Optionally, theStorageAccountAttributecan 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:
- Storage Account
- Blob
- Queue
- Table
- Other Attributes can be found in Microsoft.Azure.WebJobs.Extensions.{binding}
- Invoke the
BindAsyncmethod 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 aStreamorCloudBlockBlobtype 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
UsingBlock. In C# 8, theUsingBlocks 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 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.