Mastering Azure Serverless Computing
上QQ阅读APP看书,第一时间看更新

Creating a custom binding

You need to create a custom trigger when you want to run your Azure Function as a reaction to your custom events. In the example we saw in the previous section, we would like to run our function when the temperature of a city rises by a threshold: exceeding the threshold represents our custom event.

You must create a custom binding, instead, when you want to interact with an external data source within your Azure Function and you want to demand at runtime the responsibility for the creation and life cycle management of the binding. In that case, your function receives the instance of the binding from the runtime and doesn't care about its creation or release.

In fact, you also can interact with an external source by creating your data access class inside the body of the Azure Function (for example, using the constructor), but in this scenario, you are responsible for creating and managing the life cycle while, if you use the binding approach, you let the runtime do the job for you.

As we did for custom triggers, before starting to implement your custom binding, it's important to understand what the classes involved in the binding pipeline are and what their responsibility is in that pipeline.

The binding pipeline is similar to the custom trigger pipeline, and the involved classes depend on what kind of binding you want to implement. 

The classes involved in the pipeline are as follows:

  • BindingAttribute: This is used to decorate a function parameter to identify the function binding. It inherits from Attribute and is decorated by BindingAttribute (like the trigger attribute you saw in the previous paragraph). It has the responsibility of containing the binding data (for example, the queue name for QueueBinding) specifically for the function in which you use it.
  • BindingConfigProvider: This implements the IExtensionConfigProvider interface and has the responsibility of configuring the rules for binding. Defining a rule means that you do the following:
    • Declare what kind of binding attribute identifies the rule. When the runtime discovers that a binding attribute is used in a function, then it uses the corresponding rule.
    • Add, if you want, a validator for the attribute (for example, you can check that a connection string is formally valid).
    • Add the behavior for the rule. The behavior depends on what kind of object you, actually, want to use to implement the binding feature. You can have three types of binding:
      • BindToInput: You just declare that your binding type is an object. You use this kind of behavior when you want to bind your data to a single input (for example, a single row in CloudTable).
      • BindToCollector: You declare that you support a list in your binding; that means your binding object is a collection that implements the IAsyncCollector interface (for example, you want to add multiple items to a queue).
      • BindToStream: You want to support a stream as binding.
  • BindingConverter: This class has the responsibility of creating the actual binding class for the binding (the behavior mentioned before). It must implement the IConverter interface.
  • Binding class: The binding class is the class that actually binds to the data source. Its structure depends on the binding behavior you choose.

The following diagram shows the interactions between the classes mentioned previously in the different phases of the binding process:

As we did for the custom trigger, we'll try to implement a custom binding that allows your functions to send tweets using a Twitter account:

We'll use the TweetinviAPI package, a simple and open source library for accessing the Twitter REST API. The NuGet package is available at  https://www.nuget.org/packages/TweetinviAPI/, and the source code is available on GitHub ( https://github.com/linvi/tweetinvi).
  1. First of all, you define the binding attribute:
[Binding]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
public class TwitterBindingAttribute : Attribute
{
[AppSetting(Default = "Twitter.ConsumerKey")]
public string ConsumerKey { get; set; }

[AppSetting(Default = "Twitter.ConsumerSecret")]
public string ConsumerSecret { get; set; }

[AppSetting(Default = "Twitter.AccessToken")]
public string AccessToken { get; set; }

[AppSetting(Default = "Twitter.AccessTokenSecret")]
public string AccessTokenSecret { get; set; }
}
  1. Using this attribute, you can write the function:
[FunctionName(nameof(PeriodicTweet))]
public static void PeriodicTweet(
[TimerTrigger("0 */5 * * * *")] TimerInfo timer,
[TwitterBinding()] IAsyncCollector<string> tweetMessages,
ILogger log)
{
// .....
}

You will notice that the attribute properties are marked with the AppSetting attribute. This attribute tells the runtime that the property must be resolved by reading the values from the app settings file and, by default, the value is contained in the key defined by the Default property. So, in the previous snippet of code, the runtime will look for app settings like this:

{
"IsEncrypted": false,
"Values": {
..
"Twitter.ConsumerKey": "smv2pDr......Rbko4",
"Twitter.ConsumerSecret": "JWH8......73RUnV",
"Twitter.AccessToken": "111........EZLZv8m8w",
"Twitter.AccessTokenSecret": "E7H.....eMmKize0"
}
}

The use of the AppSetting attribute allows the developer to create code like the following:

[FunctionName(nameof(PeriodicTweet))]
public static void PeriodicTweet(
[TimerTrigger("0 */5 * * * *")] TimerInfo timer,
[TwitterBinding(ConsumerKey="myConsumerKeySetting")] IAsyncCollector<string> tweetMessages,
ILogger log)
{
// .....
}

This means that the runtime will look for the value of ConsumerKey in the myConsumerKeySettings key of the app settings.

You can also use the AutoResolve attribute, which allows the developer to use binding expressions inside the attribute declaration. For example, if you have the following code:

[Binding]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
public class TwitterBindingAttribute : Attribute
{
[AutoResolve]
public string ConsumerKey { get; set; }

....
}

You can use the following declaration:

[FunctionName(nameof(PeriodicTweet))]
public static void PeriodicTweet(
[TimerTrigger("0 */5 * * * *")] TimerInfo timer,
[TwitterBinding(ConsumerKey="%PrefixKey%%PostfixKey%")] IAsyncCollector<string> tweetMessages,
ILogger log)
{
// .....
}

This means that the runtime will look for the PrefixKey and PostfixKey in app settings and then set ConsumerKey with the concatenation of the two values.

  1. Once you have defined the attribute that identifies your binding, you must create, as you did in the previous paragraph for the custom trigger, ExtensionConfigProvider:
[Extension("Twitter")]
public class TwitterBindingConfigProvider : IExtensionConfigProvider
{
// .... Field definitions (look at GitHub repo for the full code)

public TwitterBindingConfigProvider(INameResolver nameResolver,
ILoggerFactory loggerFactory, ITwitterService twitterService)
{
// .... dependency assignment (look at GitHub repo for the full code)
}

public void Initialize(ExtensionConfigContext context)
{
var bindingRule = context.AddBindingRule<TwitterBindingAttribute>();
bindingRule.AddValidator(ValidateTwitterConfig);
bindingRule.BindToCollector<OpenType>(typeof(TwitterBindingCollectorConverter),
_nameResolver, _twitterService);
bindingRule.BindToInput<TwitterBinder>(typeof(TwitterBindingConverter),
_nameResolver, _twitterService);
}

private void ValidateTwitterConfig(TwitterBindingAttribute attribute, Type paramType)
{
// Attribute validation code
if (string.IsNullOrEmpty(attribute.AccessToken))
throw new InvalidOperationException($"Twitter AccessToken must be set either via the attribute property or via configuration.");
// ..... look at GitHub repo for the full code
}
}

In the Initialize method, you can create all the binding rules you need (using the AddBindingRule method of the  ExtensionConfigContext class). This method returns a FluuntBindingRule instance that you can use to configure the rule itself. In the previous sample, we added a validation using AddValidator and configured the rule to use the TwitterBindingCollectorConverter class and TwitterBindingConverter to create the actual binding instances. 

The validation method will be called by the runtime in the indexing phase; that is when the runtime discovers all the functions and sets up the triggers and binding. If the attribute passed by the parameter is not valid, you can throw an exception and the runtime will exclude the function from the available functions.

When the runtime tries to execute a function that uses your binding (because a trigger listener invokes it), the runtime creates the binding object using the converter you define in the BindTo method and based on what kind of binding class you request.

For example, let's say the function looks like the following:

[FunctionName(nameof(PeriodicTweets))]
public static async Task PeriodicTweets(
[TimerTrigger("0 */5 * * * *")] TimerInfo timer,
[TwitterBinding] IAsyncCollector<string> tweetMessages,
ILogger log)
{
// .....
}

The runtime uses an instance of TwitterBindingCollectorConverter to create an instance of the tweetMessages parameter:

public class TwitterBindingCollectorConverter : 
IConverter<TwitterBindingAttribute, IAsyncCollector<string>>
{
private readonly INameResolver _nameResolver;
private readonly ITwitterService _twitterService;

public TwitterBindingCollectorConverter(INameResolver nameResolver, ITwitterService twitterService)
{
_nameResolver = nameResolver;
_twitterService = twitterService;
}

public IAsyncCollector<string> Convert(TwitterBindingAttribute attribute)
{
return new TwitterBindingAsyncCollector(attribute, _twitterService, _nameResolver);
}
}

 Instead, if the function looked like the following: 

[FunctionName(nameof(PeriodicTweet))]
public static async Task PeriodicTweet(
[TimerTrigger("0 */5 * * * *")] TimerInfo timer,
[TwitterBinding()] TwitterBinder twitter,
ILogger log)
{
// .....
}

The runtime would invoke an instance of TwitterBindingConverter to create the TwitterBinder instance passed in the twitter parameter:

public class TwitterBindingConverter : 
IConverter<TwitterBindingAttribute, TwitterBinder>
{
private readonly INameResolver _nameResolver;
private readonly ITwitterService _twitterService;

public TwitterBindingConverter(INameResolver nameResolver, ITwitterService twitterService)
{
_nameResolver = nameResolver;
_twitterService = twitterService;
}

public TwitterBinder Convert(TwitterBindingAttribute attribute)
{
return new TwitterBinder(attribute, _twitterService, _nameResolver);
}
}

Both of the converters are responsible for creating the actual binding classes (they are a sort of factory) and you must implement the Convert method to create the instances of the binding objects.

The binding classes (TwitterBindingAsyncCollector and TwitterBinder in the sample) are responsible for managing the interaction between your function and the data source. In our sample, both classes send tweets (the first one allows the developer to send multiple tweets at one time, while the second one supports the sending of only one tweet at a time):

public class TwitterBindingAsyncCollector : IAsyncCollector<string>
{
// .... Field definitions (look at GitHub repo for the full code)

private readonly List<string> _tweetsToSend = new List<string>();

public TwitterBindingAsyncCollector(TwitterBindingAttribute attribute,
ITwitterService twitterService, INameResolver nameResolver)
{
// .... dependency assignment (look at GitHub repo for the full code)

this._twitterService.SetSettings(attribute);
}

public Task AddAsync(string item, CancellationToken cancellationToken = default)
{
_tweetsToSend.Add(item);
return Task.CompletedTask;
}

public async Task FlushAsync(CancellationToken cancellationToken = default)
{
foreach (var item in this._tweetsToSend)
{
await _twitterService.SendTweetAsync(item);
}
this._tweetsToSend.Clear();
}
}

TwitterBindingAsyncCollector implements IAsyncCollector<string> and it allows the developer to add messages to the tweet in the body of the function (using the Add method) and sends them when the function finishes its job (the runtime calls the FlushAsync method).  When you implement the IAsyncCollector interface, you should always implement the methods so that they are thread-safe. In the preceding example, for simplicity, the methods are not thread-safe:

public class TwitterBinder
{
// .... Field definitions (look at GitHub repo for the full code)

public TwitterBinder(TwitterBindingAttribute attribute,
ITwitterService twitterService, INameResolver nameResolver)
{
// .... dependency assignment (look at GitHub repo for the full code)

this._twitterService.SetSettings(attribute);
}

public Task TweetAsync(string message)
{
return _twitterService.SendTweetAsync(message);
}
}

The TwitterBinder class allows the developer to send a single tweet using the TweetAsync method and it doesn't implement any interfaces or base classes, so you can design your binding object as you want.

  1. Finally, you have to add the configure provider to the IWebJobBuilder instance in the host startup method (as you saw in the previous section):
[assembly: WebJobsStartup(typeof(ExtensionsStartup))]

public class ExtensionsStartup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
...
builder.AddExtension<TwitterBindingConfigProvider>();
...
}
}