SHARE:

From .NET 6 to .NET 8, my migration experience: Migrating Durable Functions .NET 8 isolated

Introduction

We recently started migrating our applications from .NET 6 to .NET 8. The migration was quite simple for all Functions that were not Durable. Durable Functions on .NET 8 isolated were often difficult. In this post I will show you all the scenarios we encountered in our Functions. If I can save you from having headaches during your migration project in 2025 I will be very happy! In this post I will show you step by step how to migrate your function!

Common changes

Before going into the details of Durable Functions we will start with the generalities associated with Azure Functions. We will see in this section the changes to be made to your NuGet packages, csproj file, your Program.cs startup file.

CsProj file

In your CsProj file, ensure that you are turning your function to .NET 8, in the v4 version and with that output type set to “Exe”, which are mandatory to run to Function to .NET 8 isolated (you can set it to .NET 9 as well).

Your PropertyGroup section should look like this:

If your functions is crashing and says “Function already exists”:

You must add in your csproj the following tag in your csproj to remove that error:

<FunctionsEnableWorkerIndexing>False</FunctionsEnableWorkerIndexing>

Nuget packages

Your project will need a ton of changes as follows:

1- Whatever the type of the function, any Nuget packages that contain “WebJobs” must be replaced as follow: From Microsoft.Azure.WebJobs.{Feature} To Microsoft.Azure.Functions.Worker.{Feature}, example:

Microsoft.Azure.WebJobs.Extensions.EventGrid becomes Microsoft.Azure.Functions.Worker.Extensions.EventGrid

2- Whatever the type of the function the following packages must be added with the Isolated mode:

You can now remove this following package which was necessary with the in-process model. It’s been replaced by the Microsoft.Azure.Functions.Worker.Sdk package for isolated model.

3- If you are using Application Insights you’ll need both packages:

4- I strongly suggest you to enable the ASP.NET Core Integration with the following package:

Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore

If you want more explanations about it, you can check my previous post: ASP.NET Core: Using the ASP.NET Core integration on Azure functions – Anthony Giretti’s .NET blog

local.settings.json file

To run Isolated functions on Visual Studio, you’ll need to turn the FUNCTIONS_WORKER_RUNTIME from dotnet to dotnet-isolated as follows:

Once you deploy it on Azure, don’t forget to update the environment variable as follows:

Program.cs file

If you ahven’t done it already, you can remove your Sartup.cs file and place everything in the new Program.cs file. The following code snippets enable the ASP.NET Core Integration and Application insights. At the end of the pipeline, you’ll have to configure logging. The later has an issue with custom logging especially with custom LogInformation and the code snippet below shows you how to overcome that:

Here is the explanation of the issue encountered with some logs: https://github.com/Azure/azure-functions-dotnet-worker/issues/2059

If you are using AppConfiguration, you can follows the steps on my previous post here: From .NET 6 to .NET 8+,my migration experience:Using Azure AppConfiguration in Azure Functions on .NET8+ isolated – Anthony Giretti’s .NET blog

Changes on ActivityTrigger

Activities undergo some changes. First, like all functions, the key name FunctionName changes to Function. Then the parameter of type IDurableActivityContext disappears in favour of the input of type that you will have specified with the function GetInput<T>(). This function is no longer necessary because the type T is, as I told you, passed as a parameter. The ActivityTrigger attribute remains unchanged. Namespaces must be adjusted, by removing all that contains “WebJob” and by adding Microsoft.Azure.Functions.Worker instead. The code below shows the code in .NET 6 in-process:

On .NET 8+ isolated the same activity gives the following:

If you activity does not use any input, you have to pass, as a parameter, the FunctionContext object instead as follows:

Changes on EntityTrigger

Here again, there are some changes. Namespaces must be adjusted by removing all containing “WebJob” and adding Microsoft.Azure.Functions.Worker instead. The EntityTrigger attribute remains unchanged while the IDurableEntityContext parameter is being replaced by TaskEntityDispatcher. The code below shows the code in .NET 6 in-process:

It gives the following on .NET 8+ isolated:

Changes on OrchestrationTrigger

OrchestrationTrigger are the most challenging part of the migration. I’ll show you an example of OrchestionTrigger I had to migrate. Many aspects have changed there, such as Entity locking, Activity invocations and its Retry Policy and finally, the function Output.

The example below shows an OrchestrationTrigger with .NET 6 in-process, I’m voluntarily missing some business logic in order to focus only on the changes required on .NET 8+ isolated:

The .NET 8+ isolated gives the following:

Let’s discuss it:

First, The interface IDurableOrchestrationContext parameters disappear, and the TaskOrchestrationContext class takes place instead. Like ActivityTrigger, the GetInput<T>() method is removed, and the T class is set as a parameter of the Orchestrator. using (await context.LockAsync(entityId)) is replaced by the await using (await context.Entities.LockEntitiesAsync(entityId)) function. The CallActivityWithRetryAsync function is removed, and the CallActivityAsync takes place instead. It will always be CallActivityAsync event if you are adding a Retry Policy or not. Note that this function is reversing the parameters (new Parameters()) and the options (taskOptions parameter). Note that the RetryOptions class is replaced by the static function TaskOptions.FromRetryPolicy() takes a RetryPolicy object as a parameter. Finally, the SetOutput function disappears and gets replaced by a string, which forces you to change the return type of the Orchestrator function from a Task to Task<string>.

Changes on DurableClient

Lastly, I’d like to show you the changes on the DurableClient. Essentially, in my experience , I only had to make two little changes. The first is IDurableOrchestrationClient parameter that is being replaced by the DurableTaskClient parameter. Then the StartNewAsync function is being replaced by the ScheduleNewOrchestrationInstanceAsync function. Here is an example of a .NET 6 in-process DurableClient:

Here the .NET 8+ isolated version of it:

Conclusion

This migration gave me a little more work than the other Azure functions to be honest. I wanted to share my migration experience with you because I had a hard time gathering all the Microsoft documentation, the latter being succinct, I had to rack my brains. I hope that, thanks to this tutorial you will be able to start 2025 without a nervous breakdown or hair pulling!
Finally, I wish you a happy and joyful end of year!

Happy New Year 2025" Images – Browse 24,041 Stock Photos, Vectors, and  Video | Adobe Stock

Written by

anthonygiretti

Anthony is a specialist in Web technologies (14 years of experience), in particular Microsoft .NET and learns the Cloud Azure platform. He has received twice the Microsoft MVP award and he is also certified Microsoft MCSD and Azure Fundamentals.