About Azure Functions
Last modified on Wed 26 Apr 2023

Azure Functions is Microsoft's serverless solution. They are, unsurprisingly, hosted on Azure, Microsoft's cloud service. Functions support several different programming languages such as C#, JavaScript, F#, Java, Python, and PowerShell.

Azure Functions need an Azure Storage account (Blob Storage, Azure Files, Queue Storage or Table Storage) when creating a function app instance. The storage account connection is used by the Functions host for operations such as managing triggers and logging function executions. It's also used when dynamically scaling function apps.

Scaling

Our scaling options will depend on the hosting plan we have for our service. There are three basic hosting plans available for Azure Functions:

Regardless of our scaling options, sometimes our requirements might lead us in the other direction. For example, if we needed queue messages to be processed by a function one at a time then we need to configure the function app to scale out to a single instance. In that case, the function's concurrency should be limited too. That is done by setting batchSize to 1 in the host.json. To learn more, check out the official documentation on Queue trigger concurrency and concurrency in Azure Functions.

Best practices

Project creation

Add a new project through the VS Function App template. When choosing the Functions worker, be sure to pick an Isolated one, other workers were applicable for previous versions. A Function project can contain multiple functions that share a common configuration such as environment variables, app settings, and host. All functions will be deployed together under the same function-app umbrella and will be scaled together.

Function app files

The default function app consists of:

Function Properties

Lifetime

Serverless functions should be short-lived and stateless. The maximum lifetime of a function can be configured using the functionTimeout property. The default lifetime is 5 minutes for Consumption (with a maximum of 10 minutes) and 30 minutes for other plans (without the upper limit). If the function execution time exceeds the configured lifetime it will be un-gracefully killed.

{
    "functionTimeout": "00:05:00"
}

Retry logic

Retry policies can be defined for all functions in an app by setting properties in host.json or for individual functions using attributes. Retry options are:

{
    "retry":{
        "strategy":"fixedDelay",
        "maxRetryCount":2,
        "delayInterval":"00:00:03"
    }
}

Some of the triggers come with default retry logic. Queue-triggered functions retry 5 times before sending a message to the poison queue, while the timer trigger function doesn't retry at all. When a time trigger function fails, it isn't called again until the next time on the schedule.

Triggers

Triggers initialize function invocation while input and output bindings allow input data to be fetched from or pushed to the source without manual integration. They eliminate the implementation of the repetitive code for integration with infrastructure, allowing developers to focus purely on business logic. Available bindings depend on the trigger type we chose. Triggers can be defined by adding the code manually, through the project creation wizard, or the "Add new item" window.

Here are some of the more popular ones:

[Function("FunctionName")]
public void Run([TimerTrigger("0 */5 * * * *")] MyInfo myTimer)
{
    _logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
    _logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus.Next}");
    // your logic
}
[FunctionName("QueueTrigger")]
public static void Run(
    [QueueTrigger("myqueue-items", Connection = "StorageConnectionAppSetting")] string myQueueItem,
    ILogger log)
{
    // your logic
}

In cases where we need to use a different storage account than other functions in the library, you can use the StorageAccount attribute. That attribute specifies the name of the configuration value that contains the storage connection string:

[StorageAccount("ClassLevelStorageAppSetting")]
public static class AzureFunctions
{
    [FunctionName("QueueTrigger")]
    [StorageAccount("FunctionLevelStorageAppSetting")]
    public static void Run(...)
    {
    // your logic
    }
}
[Function("FunctionName")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
{
    _logger.LogInformation("C# HTTP trigger function processed a request.");
    // your logic
    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
    response.WriteString("Welcome to Azure Functions!");
    return response;
}

Note: If there is already an existing App service hosting an API that needs a background job to be executed, it might be a good candidate for an Azure Web job.

Middleware

If you want to add middleware to Azure functions, all you have to do is create a new class that inherits from IFunctionsWorkerMiddleware and register it in your HostBuilder:

public class CustomMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        // code before execution
        await next(context);
        // code after execution
    }
}

Now register CustomMiddleware in the Program.cs class where you initialized your host builder:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(
        builder =>
        {
            builder.UseMiddleware<ExceptionLoggingMiddleware>();
        })
    .Build();

host.Run();

There is a limitation when using DI in this kind of middleware. You can use constructor injection just like in MVC middleware, but you can't use method injection because the Invoke method from the IFunctionsWorkerMiddleware interface accepts only two parameters.

To learn more about Azure function middlewares, read here.