Skip to main content

2 posts tagged with "azure functions"

View All Tags

C# 9 in-process Azure Functions

· 5 min read

C# 9 has some amazing features. Azure Functions are have two modes: isolated and in-process. Whilst isolated supports .NET 5 (and hence C# 9), in-process supports .NET Core 3.1 (C# 8). This post shows how we can use C# 9 with in-process Azure Functions running on .NET Core 3.1.

title image showing name of post and the Azure Functions logo

Azure Functions: in-process and isolated#

Historically .NET Azure Functions have been in-process. This changed with .NET 5 where a new model was introduced named "isolated". To quote from the roadmap:

Running in an isolated process decouples .NET functions from the Azure Functions host—allowing us to more easily support new .NET versions and address pain points associated with sharing a single process.

However, the initial launch of isolated functions does not have the full level of functionality enjoyed by in-process functions. This will happen, according the roadmap:

Long term, our vision is to have full feature parity out of process, bringing many of the features that are currently exclusive to the in-process model to the isolated model. We plan to begin delivering improvements to the isolated model after the .NET 6 general availability release.

In the future, in-process functions will be retired in favour of isolated functions. However, it will be .NET 7 (scheduled to ship in November 2022) before that takes place:

the Azure Functions roadmap image illustrating the future of .NET functions taken from https://techcommunity.microsoft.com/t5/apps-on-azure/net-on-azure-functions-roadmap/ba-p/2197916

As the image taken from the roadmap shows, when .NET 5 shipped, it did not support in-process Azure Functions. When .NET 6 ships in November, it should.

In the meantime, we would like to use C# 9.

Setting up a C# 8 project#

We're have the Azure Functions Core Tools installed, so let's create a new function project:

func new --worker-runtime dotnet --template "Http Trigger" --name "HelloRecord"

The above command scaffolds out a .NET Core 3.1 Azure function project which contains a single Azure function. The --worker-runtime dotnet parameter is what causes an in-process .NET Core 3.1 function being created. You should have a .csproj file that looks like this:

<Project Sdk="Microsoft.NET.Sdk">  <PropertyGroup>    <TargetFramework>netcoreapp3.1</TargetFramework>    <AzureFunctionsVersion>v3</AzureFunctionsVersion>  </PropertyGroup>  <ItemGroup>    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />  </ItemGroup>  <ItemGroup>    <None Update="host.json">      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>    </None>    <None Update="local.settings.json">      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>      <CopyToPublishDirectory>Never</CopyToPublishDirectory>    </None>  </ItemGroup></Project>

We're running with C# 8 and .NET Core 3.1 at this point. What does it take to get us to C# 9?

What does it take to get to C# 9?#

There's a great post on Reddit addressing using C# 9 with .NET Core 3.1 which says:

You can use <LangVersion>9.0</LangVersion>, and VS even includes support for suggesting a language upgrade.

However, there are three categories of features in C#:

  1. features that are entirely part of the compiler. Those will work.

  2. features that require BCL additions. Since you're on the older BCL, those will need to be backported. For example, to use init; and record, you can use https://github.com/manuelroemer/IsExternalInit.

  3. features that require runtime additions. Those cannot be added at all. For example, default interface members in C# 8, and covariant return types in C# 9.

Of the above, 1 and 2 add a tremendous amount of value. The features of 3 are great, but more niche. Speaking personally, I care a great deal about Record types. So let's apply this.

Adding C# 9 to the in-process function#

To get C# into the mix, we want to make two changes:

  • add a <LangVersion>9.0</LangVersion> to the <PropertyGroup> element of our .csproj file
  • add a package reference to the IsExternalInit

The applied changes look like this:

<Project Sdk="Microsoft.NET.Sdk">  <PropertyGroup>    <TargetFramework>netcoreapp3.1</TargetFramework>+    <LangVersion>9.0</LangVersion>    <AzureFunctionsVersion>v3</AzureFunctionsVersion>  </PropertyGroup>  <ItemGroup>    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />+    <PackageReference Include="IsExternalInit" Version="1.0.1" PrivateAssets="all" />  </ItemGroup>  <ItemGroup>    <None Update="host.json">      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>    </None>    <None Update="local.settings.json">      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>      <CopyToPublishDirectory>Never</CopyToPublishDirectory>    </None>  </ItemGroup></Project>

If we used dotnet add package IsExternalInit, we might be using a different syntax in the .csproj. Be not afeard - that won't affect usage.

Making a C# 9 program#

Now we can theoretically use C# 9.... Let's use C# 9. We'll tweak our HelloRecord.cs file, add in a simple record named MessageRecord and tweak the Run method to use it:

using System;using System.IO;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using Microsoft.Azure.WebJobs;using Microsoft.Azure.WebJobs.Extensions.Http;using Microsoft.AspNetCore.Http;using Microsoft.Extensions.Logging;using Newtonsoft.Json;
namespace tmp{    public record MessageRecord(string message);
    public static class HelloRecord    {        [FunctionName("HelloRecord")]        public static async Task<IActionResult> Run(            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,            ILogger log)        {            log.LogInformation("C# HTTP trigger function processed a request.");
            string name = req.Query["name"];
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();            dynamic data = JsonConvert.DeserializeObject(requestBody);            name = name ?? data?.name;
            var responseMessage = new MessageRecord(string.IsNullOrEmpty(name)                ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."                : $"Hello, {name}. This HTTP triggered function executed successfully.");
            return new OkObjectResult(responseMessage);        }    }}

If we kick off our function with func start:

screenshot of the output of the HelloRecord function

We can see we can compile, and output is as we might expect and hope. Likewise if we try and debug in VS Code, we can:

screenshot of the output of the HelloRecord function

Best before...#

So, we've now a way to use C# 9 (or most of it) with in-process .NET Core 3.1 apps. This should serve until .NET 6 ships in November 2021 and we're able to use C# 9 by default.

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

· 4 min read

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/[email protected]' = {  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: [email protected]      displayName: 'Install .NET Core SDK 3.1'      inputs:        packageType: 'sdk'        version: 3.1.x
    - task: [email protected]      displayName: 'Install .NET SDK 5.0'      inputs:        packageType: 'sdk'        version: 5.0.x
    - task: [email protected]      displayName: "function app test"      inputs:        command: test            - task: [email protected]      displayName: "function app build"      inputs:        command: build        arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)/MyApp'            - task: [email protected]      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!