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
IBinder
orBinder
instance to the Function’s parameter. The binding parameters can now be replaced with a singleIBinder
orBinder
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) { ... }
- Create the Attribute Object Instead of declaring the arguments in the parameter attribute. An
Attribute
object must now be created, it can also be anAttribute[]
. For the Blob Binding, aBlobAttribute
must be initialized with the Path and Direction. Optionally, theStorageAccountAttribute
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:
- Storage Account
- Blob
- Queue
- Table
- Other Attributes can be found in Microsoft.Azure.WebJobs.Extensions.{binding}
- 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 aStream
orCloudBlockBlob
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, theUsing
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 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.