Azure Functions imperative bindings

pgroenewegen@xpirit.com

The standard input and output bindings in Azure Functions are written in a declarative pattern using the function.json. When defining input and output declarative, you do not have the option to change some of the bindings properties like the name or make multiple outputs from one input. An imperative binding can do this for you. In this blog post I’ll show how to use imperative blob bindings.

Imperative binder pattern
The imperative binder uses a pattern where you add the Binder object in the signature of your Run method. In the function you use attributes to bind the output to the binder. You can bind multiple outputs to the binder, and you are able to combine a declarative binding with the imperative binding. In this case the BlobTrigger is defined in function.json. Do not include the output binding in you function.json:

public static async Task Run(string input, Binder binder)  
{
  ...
  var attributes = new Attribute[]
  {    
    new BlobAttribute("samples-output/path"),
    new StorageAccountAttribute("MyStorageAccount")
  };
  T output = await binder.BindAsync<T>(attributes);
  output. ...;
}
{
  "bindings": [
    {
      "name": "input",
      "type": "blobTrigger",
      "direction": "in",
      "path": "test/{name}",
      "connection": "<storage connection>"
    }
  ],
  "disabled": false
}

Rename incoming blob
Renaming a blob is basically a two step process. The first step is to copy the content of the blob into a new blob file and the second step is to delete the original blob.
Copy blob
The first step is to copy the blob into a new blob. The new blob name is based on the current datetime in the path. The following code shows how to do this with a binder:

#r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Blob;
public static async Task Run(string input, string name,  Binder binder, TraceWriter log)
{
    log.Info($"C# Blob trigger function processed: {name} ({input.Count()})");
    string path = $"testcontainer/{DateTime.UtcNow.ToString("yyyy/MM/dd/HH")}/"+name;
    
    var attributes = new Attribute[]
    {    
        new BlobAttribute(path),
        new StorageAccountAttribute("<storage connection>")
    };

    using (var writer = await binder.BindAsync<TextWriter>(attributes))
    {
        writer.Write(input);
    }
}

The function.json is the same as in sample 1.

Delete blob
Deleting the original blob can be done declarative and imperative. I’ll show both:

Delete blob declarative
All parameters are declared in the function.json:

{
  "bindings": [
    {
      "name": "input",
      "type": "blobTrigger",
      "direction": "in",
      "path": "test/{name}",
      "connection": "<storage connection>"
    },
    {
        "type": "output",
        "name": "output",
        "direction": "inout",
        "path": "test/{name}",
        "connection": "<storage connection>"
    }
  ],
  "disabled": false
}
#r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Blob;
public static async Task Run(string input, string name, CloudBlockBlob output, TraceWriter log)
{
    log.Info($"C# Blob trigger function processed: {name} ({input.Count()})");
    
    output.Delete();
}

This will delete the incoming blob file (the path in the function.json is the same for the input and output).

Delete blob You can also delete the incoming blob imperative using the Binder:

{
  "bindings": [
    {
      "name": "input",
      "type": "blobTrigger",
      "direction": "in",
      "path": "test/{name}",
      "connection": "<storage connection>"
    }
  ],
  "disabled": false
}
#r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Blob;
public static async Task Run(string input, string name, Binder binder, TraceWriter log)
{
    log.Info($"C# Blob trigger function processed: {name} ({input.Count()})");
    
    var attributesOrg = new Attribute[]
    {    
        new BlobAttribute($"test/{name}"),
        new StorageAccountAttribute("<storage connection>")
    };

    var blob = await binder.BindAsync<CloudBlockBlob>(attributesOrg);
    blob.Delete();
}

Move/rename blob
Combining the copy and delete give the following code to move/rename the blob:

{
  "bindings": [
    {
      "name": "input",
      "type": "blobTrigger",
      "direction": "in",
      "path": "test/{name}",
      "connection": "<storage connection>"
    }
  ],
  "disabled": false
}
#r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Blob;
public static async Task Run(string input, string name, Binder binder, TraceWriter log)
{
    log.Info($"C# Blob trigger function processed: {name} ({input.Count()})");
    
    //Copy blob
    string path = $"testcontainer/{DateTime.UtcNow.ToString("yyyy/MM/dd/HH")}/"+name;
    
    var attributes = new Attribute[]
    {    
        new BlobAttribute(path),
        new StorageAccountAttribute("<storage connection>")
    };
    using (var writer = await binder.BindAsync<TextWriter>(attributes))
    {
        writer.Write(input);
    }

    //Delete the blob
    var attributesOrg = new Attribute[]
    {    
        new BlobAttribute($"test/{name}"),
        new StorageAccountAttribute("<storage connection>")
    };
    var blob = await binder.BindAsync<CloudBlockBlob>(attributesOrg);
    blob.Delete();
}

In the sample code the are multiple (output)actions bind to the Binder.

Make multiple blobs depending on incoming blob
Another use case for the imperative binder can be creating multiple output blobs depending on the incoming blob. You can call the binder multiple time to save all the blobs. As sample I make a new blob for each line in the incoming blob file.

{
  "bindings": [
    {
      "name": "blob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "test/{name}",
      "connection": "<storage connection>"
    }
  ],
  "disabled": false
}
#r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Blob;
public static async Task Run(string input, string name, Binder binder, TraceWriter log)
{
    log.Info($"C# Blob trigger function processed: {name} ({input.Count()})");
    
    int linecount = 0;
    foreach (var line in input.Split('n')){
        string path = $"testcontainer/lines/{linecount}/{name}";
        log.Info($"path: {path}");

        var attributes = new Attribute[]
        {    
            new BlobAttribute(path),
            new StorageAccountAttribute("<storage connection>")
        };

        using (var writer = await binder.BindAsync<TextWriter>(attributes))
        {
            writer.Write(line);
        }
        linecount++;
    }
}

Conclusion
The advanced Binder enables you to make outputs that are based on computations in the function itself. In this sample the blob bindings are used. The same is possible for all other binding types:

Binding Attribute Add reference
DocumentDB Microsoft.Azure.WebJobs.DocumentDBAttribute #r "Microsoft.Azure.WebJobs.Extensions.DocumentDB"
Event Hubs Microsoft.Azure.WebJobs.ServiceBus.EventHubAttribute, Microsoft.Azure.WebJobs.ServiceBusAccountAttribute #r "Microsoft.Azure.Jobs.ServiceBus"
Mobile Apps Microsoft.Azure.WebJobs.MobileTableAttribute #r "Microsoft.Azure.WebJobs.Extensions.MobileApps"
Notification Hubs Microsoft.Azure.WebJobs.NotificationHubAttribute #r "Microsoft.Azure.WebJobs.Extensions.NotificationHubs"
Service Bus Microsoft.Azure.WebJobs.ServiceBusAttribute, Microsoft.Azure.WebJobs.ServiceBusAccountAttribute #r "Microsoft.Azure.WebJobs.ServiceBus"
Storage queue Microsoft.Azure.WebJobs.QueueAttribute, Microsoft.Azure.WebJobs.StorageAccountAttribute
Storage blob Microsoft.Azure.WebJobs.BlobAttribute, Microsoft.Azure.WebJobs.StorageAccountAttribute
Storage table Microsoft.Azure.WebJobs.TableAttribute, Microsoft.Azure.WebJobs.StorageAccountAttribute
Twilio Microsoft.Azure.WebJobs.TwilioSmsAttribute #r "Microsoft.Azure.WebJobs.Extensions.Twilio"

When making the right choice between an imperative or declarative binding keeps the code clean. This will keep the function code maintainable and future proof.

Comments (0)

    Add a Comment