Azure Functions v3 migration guide from .NET Core 3.1 to Isolated Model on .NET 5

Adrien Pellegrini
8 min readJul 15, 2021

This guide will help you migrate from a project with Azure Functions v3 on .NET Core 3.1 to the new Azure Functions Isolated Model on .NET 5.

⚠ ⚠ ⚠ This guide doesn’t provide an exhaustive list of steps. It is based on my own migration and cover only a very small subset of all the possibilities offered by the Azure Functions. Some information may not be accurate anymore. ⚠ ⚠ ⚠

Step 0 — Environment checks

The very first step is to install the .NET 5 SDK.

The second important step is to ensure that the Azure Functions Core Tools version is at greater than 3.0.3477 (func --help).

Then you should edit your global.json file, if any. To get which version to use, you can run the command-line dotnet --info and pick the latest version of .NET 5 SDK installed.

from

{
"sdk": {
"version": "3.1.404",
"rollForward": "latestFeature"
}
}

to

{
"sdk": {
"version": "5.0.301",
"rollForward": "latestFeature"
}
}

Step 1 — Update the CSPROJ

Update the CSPROJ file:

  • to target the .NET 5 framework
  • switch the assembly output to “Exe”
  • use the new NuGet packages

from

<TargetFramework>netcoreapp3.1</TargetFramework>

to

<TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType>

If you encounter this following error, you need either to add the SelfContained property or the OutputType property (as shown in the previous snippet).

1>C:\Users\adrien\.nuget\packages\microsoft.azure.functions.worker.sdk\1.0.1\build\Microsoft.Azure.Functions.Worker.Sdk.targets(51,9): error MSB4113: Specified condition “$(SelfContained)” evaluates to “” instead of a boolean.

Then we update the NuGet packages:

from

<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />

to

<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.0.3" OutputItemType="Analyzer" /><PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.3.0" />

It’s also a good time to update other Microsoft.* packages to use the .NET 5 version.

Step 2 — Add a new entry point

Like any other ASP.NET core app project, we need to provide an entry point to define and start a host:

At this point, you should be able to run the project. But no function will be available.

Step 3 — Update the settings files

Update within local.settings.json the FUNCTION_WORKER_RUNTIME from dotnet to dotnet-isolated.

Step 4 — Fix namespace

Use the new namespace from the Azure Function Worker:

from

using Microsoft.Azure.WebJobs;

to

using Microsoft.Azure.Functions.Worker;

Step 5 — Fix FunctionName attribute

Rename [FunctionName("xxx")] attribute on each function to [Function("xxx")].

The attribute is provided by the Microsoft.Azure.Functions.Worker.

Step 6 — Add extensions NuGet packages related to your usage

For HttpTrigger, add Microsoft.Azure.Functions.Worker.Extensions.Http.

For ServiceBugTrigger, add Microsoft.Azure.Functions.Worker.Extensions.ServiceBus.

Convert all namespaces containing WebJobs their Worker equivalent.

For HttpTrigger:

using Microsoft.Azure.WebJobs.Extensions.Http;

to

using Microsoft.Azure.Functions.Worker.Http;

Step 7 — Remove additional *.WebJobs.* packages and namespaces

Some packages use WebJobs packages internally and should be removed. For example Microsoft.Azure.Functions.Extensions contains FunctionInvocationException (if you catch globally all exceptions).

Some WebJobs packages have helpers around IConfiguration and IConfigurationRoot. So if you don’t have access to some methods you probably need to add Microsoft.Extensions.Configuration.Binder or Microsoft.Extensions.Configuration.Abstractions packages.

Step 8 — Convert ExecutionContext

The classExecutionContext has been renamed to FunctionContext.

context.FunctionName property is now context.FunctionDefinition.Name.

If you used ExecutionContext in a class library targeting netstandard2.0 you would need to switch it to .NET 5 as the Worker and Worker.Core target both .NET 5.

Step 9/A — HTTP

New package:

Replace HttpRequestMessage and HttpResponseMessage by HttpRequestDataand HttpResponseData:

from

public async Task<HttpResponseMessage> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = FunctionRoute)] HttpRequestMessage req)
{
}

to

public async Task<HttpResponseData> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = FunctionRoute)] HttpRequestData req)
{
}

HttpRequestData is now in a package targeting .NET 5, so be careful if you used HttpRequestMessage in a library targeting netstandard2.0.

Request URI:

req.RequestUri is now req.Url.

Returning result (as JSON) with HTTP status code:

There are extensions methods provided by HttpResponseDataExtensions to build the response from the request.

In Azure Function on .NET Core 3.1, this following code snippet that returns a JSON object serialized as a StringContent within the response:

var response = new HttpResponseData(httpCode);response.RequestMessage = request;response.Content = new StringContent(json, Encoding.UTF8, "application/json");return response;

need to be converted to:

var response = request.CreateResponse(httpCode);response.Headers.Add("Content-Type", "application/json");await response.WriteStringAsync(json, Encoding.UTF8);return response;

Or you can use directly the WriteAsJsonAsync extension methods. The WriteAsJsonAsync add the application/json header. So don’t add it again or you will get an error.

⚠ ⚠ ⚠ In the current release, be careful about WriteAsJsonAsync, this method updates the HTTP status code to 200 OK. If you use another status code within the constructor, it will be overwritten (see GitHub issue). ⚠ ⚠ ⚠

Serialization:

The serializer can be configured within the WorkerOptions. Internally the method WriteAsJsonAsync configure the serializer as following:

public static ValueTask WriteAsJsonAsync<T>(this HttpResponseData response, T instance, string contentType, CancellationToken cancellationToken = default(CancellationToken))
{
...
ObjectSerializer serializer = response.FunctionContext.InstanceServices.GetService<IOptions<WorkerOptions>>()?.Value?.Serializer ?? throw new InvalidOperationException("A serializer is not configured for the worker.");return response.WriteAsJsonAsync(instance, serializer, contentType, cancellationToken);
}

Route data:

Extracting route data (MS_AzureWebJobs_HttpRouteData) is a easier now; instead of req.Properties.TryGetValue(“HttpContext”, out var httpContext) we can simply retrieve the information from req.FunctionContext.BindingContext.BindingData.

Let’s define an HttpTrigger function with the route service/{routeParam1}/test/{routeParam2}/end-call.

If you try calling function with some query parameters and a JSON:

http://localhost:7071/api/service/valueParam1/test/valueParam2/end-call?queryParam1=abcd&queryParam2=1234 with body

{
"bodyValue1": "xxxx",
"bodyValue2": true,
"bodyValue3": 3,
"bodyValue4": {
"innerBodyValye4": "yyyy"
}

you will get a dictionary containing:

To extract only the route parameters, we can easily match which parameter belongs to the route by parsing the function route.

Identity:

The ClaimsPrincipal passed as a parameter of the Function can be removed and replaced by the req.Identities (from HttpRequestData).

Useful links:

Step 9/B — ServiceBus

New package:

The only possible way, for the moment (at least until .NET 6.0 InProc version), to read the service bus message is to get the binding data data. You get the message content as a string by adding a parameter to the method.

[Function("FunctionName")]
public static async Task Run(
[ServiceBusTrigger(
queueName: "queueNameInAzure",
Connection = "my-localsetting-connection"
)] string data,
FunctionContext functionContext,
string contentType,
string lockToken)
{
}

MessageReceiver cannot be used for the moment. If we try to Complete a message we have the usual error:

The lock supplied is invalid. Either the lock expired, or the message has already been removed from the queue, or was received by a different receiver instance.

FunctionContext/BindingData:

It seems that the properties of the message are splitted inside the BindingData so, in the case of the CloudEvent formatted message I have sent over the bus, we can access directly “root” properties with functionContext.BindingContext.BindingData:

Settings:

Unfortunately, setting binding using the Azure App Configuration is not working for the moment. So you cannot write:

[ServiceBusTrigger(
topicName: "%bus-sample/my-topic%",
subscriptionName: "%bus-sample/my-subscription%",
Connection = "%bus-sample/connection%"
)]

Message autocompletion:

⚠ ⚠ ⚠ This doesn’t work for the moment. ⚠ ⚠ ⚠

But we still need to “fix” the code to make it compiles correctly. So we can either remove it or change it and fix it later when this feature will be fully available.

If you autocomplete messages, you probably have the following code or similar in the startup:

builder.Services.Configure<ServiceBusOptions>(options =>
{
options.MessageHandlerOptions.AutoComplete = false;
});

The ServiceBusOptions is not available anymore; so we need to add the Azure.Messaging.ServiceBus package:

And change the configuration to:

services.Configure<ServiceBusProcessorOptions>(options =>
{
options.AutoCompleteMessages = false;
});

An additional and far more complete migration guide is provided at: azure-sdk-for-net/MigrationGuide.md at master · Azure/azure-sdk-for-net (github.com).

Step 9/C — EventHub

New package:

The old package has a reference to Microsoft.Azure.EventHubs:

To get back the EventData class, you need to install the new package Azure.Messaging.EventHubs:

and update the namespace from

using Microsoft.Azure.EventHubs;

to

using Azure.Messaging.EventHubs;

The eventData.Body is now eventData.EventBody.

Step 10 — [NotAutomaticTrigger] attribute

This attribute doesn’t exist anymore. To ease the migration I would suggest creating your own for the moment. You can remove it later as it is now useless.

using System;namespace Microsoft.Azure.Functions.Worker
{
public class NoAutomaticTriggerAttribute : Attribute
{
}
}

Step 11 — Convert your Startup.cs class

If you used a custom Startup class to configure your services for dependency injection; you probably need to split it into the new IHostBuilder methods:

  • ConfigureLogging()
  • ConfigureAppConfiguration()
  • ConfigureServices()

These methods can be called multiple times at different places; this is pretty convenient to split configuration files neatly.

If you have some extensions methods in netstandard2.0 libraries, you will probably need to target net5.0 because of the IHostBuilder. This interface comes with the Microsoft.Azure.Functions.Worker.Core package.

The startup class need to be converted:

from

to

We can then update the HostBuilder inProgram.cs and call the new ConfigureCustomFunctionsWorker() method:

Step 12 — ApplicationInsights

First we need to use another NuGet packages:

from

<PackageReference 
Include=”Microsoft.Azure.WebJobs.Logging.ApplicationInsights”
Version=”3.0.23" />

to

<PackageReference 
Include=”Microsoft.ApplicationInsights.WorkerService”
Version=”2.17.0" />

And then we need to update the services registration:

from

services.AddApplicationInsightsTelemetry();

to

services.AddApplicationInsightsTelemetryWorkerService();

Step 13 — Feature Flags

Replace IConfigurationRefresher by IConfigurationRefresherProvider.

Additional Step — Convert old code to .NET 5

Some parts of the code have been moved to their specific NuGet packages and need to be migrated to their .NET 5 equivalent.

Some examples are

Additional Links

.NET isolated process guide for .NET 5.0 in Azure Functions | Microsoft Docs

--

--