Friday, 11 June 2021

Azure Functions and .NET 5: Query params, Dependency Injection, Bicep & Build

The upgrade of Azure Functions from .NET Core 3.1 to .NET 5 is significant. There's an excellent guide for the general steps required to perform the upgrade. However there's a number of (unrelated) items which are not covered by that post:

  • Query params
  • Dependency Injection
  • Bicep
  • Build

This post will show how to tackle these.

title image showing name of post and the Azure Functions logo

Query params

As part of the move to .NET 5 functions, we say goodbye to HttpRequest and hello to HttpRequestData. Now HttpRequest had a useful Query property which allowed for the simple extraction of query parameters like so.

var from = req.Query["from"]

HttpRequestData has no such property. However, it's straightforward to make our own. It's simply a matter of using System.Web.HttpUtility.ParseQueryString on req.Url.Query and using that:

var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
var from = query["from"]

Dependency Injection, local development and Azure Application Settings

Dependency Injection is a much more familiar shape in .NET 5 if you're familiar with .NET Core web apps. Once again we have a Program.cs file. To get the configuration built in such a way to support both local development and when deployed to Azure, there's a few things to do. When deployed to Azure you'll likely want to read from Azure Application Settings:

screenshot of Azure Application Settings

To tackle both of these, you'll want to use AddJsonFile and AddEnvironmentVariables in ConfigureAppConfiguration. A final Program.cs might look something like this:

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MyApp
{
    public class Program
    {
        public static Task Main(string[] args)
        {
            var host = new HostBuilder()
                .ConfigureAppConfiguration(configurationBuilder =>
                    configurationBuilder
                        .AddCommandLine(args)
                        // below is for local development
                        .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                        // below is what you need to read Application Settings in Azure
                        .AddEnvironmentVariables()
                )
                .ConfigureFunctionsWorkerDefaults()
                .ConfigureServices(services =>
                {
                    services.AddLogging();
                    services.AddHttpClient();
                })
                .Build();

            return host.RunAsync();
        }
    }
}

With this approach in place, when the application runs, it should construct a configuration driven by all the providers required to run our application.

Bicep

When it comes to deploying to Azure via Bicep, there's some small tweaks required:

  • appSettings.FUNCTIONS_WORKER_RUNTIME becomes dotnet-isolated
  • linuxFxVersion becomes DOTNET-ISOLATED|5.0

Applied to the resource itself the diff looks like this:

resource functionAppName_resource 'Microsoft.Web/sites@2018-11-01' = {
  name: functionAppName
  location: location
  tags: tags_var
  kind: 'functionapp,linux'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlanName_resource.id
    siteConfig: {
      http20Enabled: true
      remoteDebuggingEnabled: false
      minTlsVersion: '1.2'
      appSettings: [
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~3'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
-          value: 'dotnet'
+          value: 'dotnet-isolated'
        }
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(resourceId('Microsoft.Storage/storageAccounts', storageAccountName), '2019-06-01').keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
        }
      ]
      connectionStrings: [
        {
          name: 'TableStorageConnectionString'
          connectionString: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(resourceId('Microsoft.Storage/storageAccounts', storageAccountName), '2019-06-01').keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
        }
      ]
-      linuxFxVersion: 'DOTNETCORE|LTS'
+      linuxFxVersion: 'DOTNET-ISOLATED|5.0'
      ftpsState: 'Disabled'
      managedServiceIdentityId: 1
    }
    clientAffinityEnabled: false
    httpsOnly: true
  }
}

Building .NET 5 functions

Before signing off, there's one more thing to slip in. When attempting to build .NET 5 Azure Functions with the .NET SDK alone, you'll encounter this error:

The framework 'Microsoft.NETCore.App', version '3.1.0' was not found.

Docs on this seem to be pretty short. The closest I came to docs was this comment on Stack Overflow:

To build .NET 5 functions, the .NET Core 3 SDK is required. So this must be installed alongside the 5.0.x sdk.

So with Azure Pipelines you might have have something that looks like this:

stages:
  - stage: build
    displayName: build
    pool:
      vmImage: 'ubuntu-latest'
    jobs:
      - job: BuildAndTest
        displayName: 'Build and Test'
        steps:
          # we need .NET Core SDK 3.1 too!
          - task: UseDotNet@2
            displayName: 'Install .NET Core SDK 3.1'
            inputs:
              packageType: 'sdk'
              version: 3.1.x

          - task: UseDotNet@2
            displayName: 'Install .NET SDK 5.0'
            inputs:
              packageType: 'sdk'
              version: 5.0.x

          - task: DotNetCoreCLI@2
            displayName: 'function app test'
            inputs:
              command: test

          - task: DotNetCoreCLI@2
            displayName: 'function app build'
            inputs:
              command: build
              arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)/MyApp'

          - task: DotNetCoreCLI@2
            displayName: 'function app publish'
            inputs:
              command: publish
              arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)/MyApp /p:SourceRevisionId=$(Build.SourceVersion)'
              publishWebProjects: false
              modifyOutputPath: false
              zipAfterPublish: true

          - publish: $(Build.ArtifactStagingDirectory)/MyApp
            artifact: functionapp

Have fun building .NET 5 functions!