This article is part of #ServerlessSeptember. You'll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles are published every day — that's right, every day — from community members and cloud advocates in the month of September.
Find out more about how Microsoft Azure enables your Serverless functions at https://docs.microsoft.com/azure/azure-functions/.
Serverless functions rarely work in isolation. Functions are often paired with other services like a database, a storage service, or perhaps even an email notification service. Azure Functions makes it easy to connect with these services by offering bindings that give you an out-of-the-box connection to an array of cloud services. You can achieve a lot using the standard configurations, but what if you need go beyond the defaults?
Suppose you want to:
- Change a storage location depending on an incoming IP address?
- Switch an email recipient based on a dev vs. production environment?
- Write data to a different message queue based on a date range?
These are just a few examples of the type of adjustments which may be difficult using a binding's default settings. This article demonstrates how to configure a C# Azure Functions binding at run time to take full control over a binding's configuration.
tl;dr
To configure a binding at runtime:
- Declare the binding parameter as a Binder or IBinder instance.
- Create an instance of the binding attribute.
- Customize the binding attribute as necessary.
- Either use
Bind
orBindAsync
to bind the attribute to a cloud service. - Use the binding
A working example is available on GitHub: azure-functions-runtime-binding.
Default binding
Before diving into the details of how runtime binding works, first consider the default scenario. The following code example implements an HTTP-triggered function that saves a message to an Azure Storage blob container.
[FunctionName("SaveDefault")]
public static IActionResult Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
HttpRequest req,
[Blob("messages/{sys.randguid}.txt", FileAccess.Write)] out string blob,
ILogger log)
{
string message = req.Query["message"];
blob = $"Default binding: {message}";
return new OkResult();
}
For the binding to work in this context, the blob
parameter is set up in the following ways:
- The
Blob
attribute is declared to write data mapped to a blob container namedmessages
. - The
{sys.randguid}
replacement token ensures the new blob is given a unique name. - The parameter is declared as a
string
. - The parameter is declared as an
out
parameter. Using an out parameter gives the binding a chance to take the primitivestring
's value and use it to write out to the storage container elsewhere in the binding's logic.
This setup works great for simple scenarios. However, using this approach makes it difficult to make changes to the binding. In some cases you may want to:
- Change the connection string.
- Calculate the blob name and/or the container's name.
- You do have a few replacement tokens and route parameters available to influence the blob path, but some needs are more complex.
- Dynamically change the file access type. Perhaps in some instances you want to create the attribute with
Write
orReadWrite
access levels.
To achieve more flexible binding customization, you can implement runtime binding.
Runtime binding
The final result of the following function is the same as implemented for the default scenario. The difference in this instance is that the binding is imperatively created, giving you the chance to affect a binding's behavior.
[FunctionName("SaveCustom")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
HttpRequest req,
IBinder binder,
ILogger log)
{
string message = req.Query["message"];
var attribute = new BlobAttribute("messages/{sys.randguid}.txt", FileAccess.Write);
attribute.Connection = "AzureStorageConnectionString";
using(var blob = await binder.BindAsync<TextWriter>(attribute))
{
blob.Write($"Runtime binding: {message}");
}
return new OkResult();
}
The binding is declared as an IBinder
type, which gives you the chance to determine the binding configuration.
In this scenario:
- An instance of
BlobAttribute
is created by passing the same parameters to the constructor as shown in the default example.- Here you could decide to use custom logic to determine the blob container name or generate a custom blob name.
- The
Connection
property is set.- Here you could swap out an app setting name depending on the environment or perhaps input to the function.
- The blob binding is created by calling
binder.BindAsync
since theTextWriter
class is used to write to the blob container. - Calling
blob.Write
is what eventually persists the data to the blob container.
NOTE: Instead of typing the binding as a
string
(as shown in the default scenario), the binding is created as aTextWriter
. TheTextWriter
class is used because astring
instance has no knowledge of how to write back to the blob container. TheTextWriter
is configured to know about the blob container through the call ofbinder.BindAsync
.
Code sample
The sample code is available on GitHub: azure-functions-runtime-binding.
Summary
The built-in Azure Functions bindings give you rich access to a number of different cloud services. While the default behavior may work well in many cases, sometime you need the chance to customize a binding's configuration.
You can customize a binding at runtime via the following steps:
- Declare the binding parameter as a Binder or IBinder instance.
- Create an instance of the binding attribute.
- Customize the binding attribute as necessary.
- Either use
Bind
orBindAsync
to bind the attribute to a cloud service. - Use the binding