Chat with us, powered by LiveChat

See how Adaptiv can transform your business. Schedule a kickoff call today

How to Upgrade Azure Functions to .NET 8 (Isolated Worker Model): A Guide for Junior Devs

  • Technical

Introduction

You’ve been assigned the ticket, read the vendor guide on the process, and cracked in to refactoring some of your code base. Everything is going well until you google your first error message and get no results (or almost worse, getting too many). If this is you, specifically in the context of upgrading Azure functions to .NET 8 and the Isolated Worker model, hopefully I can help. 

This guide aims to shed some light on the reasoning behind some of the things this other guide (“Migrate .NET function apps..”) says to do, and illuminate some stumbling blocks I wish I had known about.

A bit of background

An Azure function is a serverless piece of code you can host on the Azure platform that comes with an inbuilt suite of triggers and bindings for Azure services. These can be written in a variety of languages, including C#; and if your function is written in .NET 6 C#, you’ll know it is coming to the end of support 12/11/24. This is where the ‘8’ part of the title comes in, as .NET 8 is in long-term support until the end of 2026, so you should make sure to migrate your code. 

Additionally, Microsoft suggests adopting the Isolated Worker Model, which allows your function code to run in a separate .NET worker process, which has a variety of benefits compared to the in-process model.

Note: Upgrade Wizard

If you do your local development in Visual Studio, it has an upgrade wizard that can help upgrade a project’s .NET version. This will handle the boilerplate and easy renaming of things, but will not provide end-to-end upgrade of broken packages, or any manual refactoring you will have to do.

Part 1: Migrating your local project 

Step 1: .csproj project file

Since the .csproj file contains the build instructions for your project, it is an obvious place to start. 

You replace: 

  • The target framework (net8.0) 
  • The AzureFunctionsVersion (v4) 
  • Any Microsoft.NET.Sdk.Functions.* with their equivalent Microsoft.Azure.WebJobs.* namespaces*. 

You add: 

  • An output type (Exe). This changes the output type from a code library to a console application. 
  • Include = “System.Threading.ExecutionContext”, this allows for the passing context between the host and application threads, now they are separate. 

*This is also a great time to update any package references in your code while it’s fresh in your mind which ones they were, noting any breaking changes between versions. 

Step 2: Program.cs file 

In .NET 6 the presence of a Startup.cs file was not mandatory, so when the guide tells you you now need a Program.cs file to replace it, you might be thinking, replace what? 

The Program.cs file is the entry point for running your project. It lets you create services for dependency injection, configure log filtering rules, and do things like set up your default JSON serialiser. 

The example program.cs file from the guide will certainly get you started, but I’ll show some examples of what you can do. 

using ...


var host = new HostBuilder()
//using ConfigureFunctionsWebApplication because of ASP.NET Core integration, otherwise use ConfigureFunctionsWorkerDefaults()
   .ConfigureFunctionsWebApplication((IFunctionsWorkerApplicationBuilder workerApplication) =>
 {
  //This sets the default JSON serializer to the Newtonsoft one
  workerApplication.Services.AddMvc().AddNewtonsoftJson();
 })
   .ConfigureServices(services =>
   {
       // Logging setup
       services.AddApplicationInsightsTelemetryWorkerService(options =>
       {
           options.EnableAdaptiveSampling = false;
       });
       services.ConfigureFunctionsApplicationInsights();
       services.Configure<LoggerFilterOptions>(options =>
       {
  //This line overrides the inbuilt rule to ignore anything under 'Warning' level
           LoggerFilterRule defaultRule = options.Rules.FirstOrDefault(rule => rule.ProviderName == "Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider");
           if (defaultRule is not null)
           {
               options.Rules.Remove(defaultRule);
           }
       });

       // Adding custom services for dependency injection
       services.AddSingleton<IAssignmentGroupMatcherService, AssignmentGroupMatcherService>();

// Adding Cosmos Client
       services.AddSingleton(serviceProvider =>
       {
  //Pre-provisioning containers for the client
           var containers = new List<(string, string)>
           {
             ("XXX", "YYY"),
             ("XXX", "ZZZ")
           };

           var options = new CosmosClientOptions();
           options.ConnectionMode = ConnectionMode.Gateway;
  //Environment.GetEnvironmentVariable("xxx") retrieves value from local.settings.json 'Values['xxx]' object
           var client = CosmosClient.CreateAndInitializeAsync(Environment.GetEnvironmentVariable("CosmosConnectionString"), containers, options).GetAwaiter().GetResult();
           return client;
       });

//Adding a ServiceBusClient using AddAzureClientMethod
       services.AddAzureClients(builder =>
       {
           builder.AddServiceBusClient(Environment.GetEnvironmentVariable("ServiceBusConnection"));
       }
       );
   })
   .ConfigureLogging((hostingContext, logging) =>
   {
       //If your appsetting.json file is at root, the hostingContext will automatically pick it up.
//In appsettings.json, just make sure you're JSON key is not the same as the key in host.json as there could be a collision
       logging.AddConfiguration(hostingContext.Configuration.GetSection("WorkerLogging"));
   })
   .Build();

host.Run();

Step 3: Function signature changes

Some changes between the in-process and isolated worker model affect certain function attributes, parameters and return types. This can be exacerbated if the function you are refactoring uses a now deprecated SDK. 

 The main ones I had to worry about were: 

  • Logging. If you used an ILogger parameter to your function, you will need to switch to dependency injection.  See the Program.cs above for an example of how to configure this at startup. As noted in the guide, doing so this way allows more fine grained control of your logging, as any configuration you put in a host.json file will not affect logs coming directly from your application. 
  • Trigger and Binding Changes. The official guide provides all the necessary steps for this, when it suggests “replacing the output binding with a client object for the service it represents, … by injecting a client yourself.” Examples for creating Service Bus and Cosmos clients for later injection are provided in the Program.cs example.

Step 4: local.settings.json file 

When debugging your function locally, one option is to use the Azurite Emulator and set: 

“AzureWebJobsStorage”: “UseDevelopmentStorage=true” in your local.settings.json file. 

If, like me, you end up having issues with this (particularly because Timer Triggers need access to a storage account), remember that that is shorthand for the default account the emulator uses, for some reason setting the explicit longform version fixed issues with creating a local storage emulation. 

Step 5: JSON Serialisation 

The guide notes that the default JSON serialiser for the isolated worker model is System.Text.Json. That means that if you had previously been using Newtonsoft.Json objects in your bindings, and have not set the serialiser, you may experience serialisation/deserialisation errors where it looks like your code is accepting/returning a valid json object, but the binding is throwing errors in the background. 

For an example of how to set the serialiser, see the above program.cs*

*There have been issues with serialisers for bindings, the way we have done it not exactly the way Microsoft suggests, but a workaround.

Part 2:  Update your function app in Azure 

This section heavily depends on what method you use to deploy your function app, and should hopefully be a lot smoother than the first part. The method I used was through source code/pipeline deployment. The problem I ran into with this step was forgetting to update the code for the infrastructure that the function was running on. 

In my specific case, it was making sure the netFrameworkVersion of the “Host” function app was set correctly, and that the FUNCTIONS_WORKER_RUNTIME matched what I was trying to deploy. 

// Function App 
resource functionApp 'Microsoft.Web/sites@2022-03-01' = { 
 name: functionAppName 
 kind: 'functionapp' 
 location: resourceGroup().location 
} 
 properties: { 
... 
  siteConfig: { 
 ... 
  netFrameworkVersion: 'v8.0' 
  appSettings: [ 
    { 
      name: 'FUNCTIONS_WORKER_RUNTIME' 
      value: 'dotnet-isolated' 
    } 
  ] 
} 
} 
 ... 
} 

Part 3

Congratulations, there is no part 3! If you have tested your function app is running correctly in the cloud, pat yourself on the back, and go update that ticket.

Ready to elevate your data transit security and enjoy peace of mind?

Click here to schedule a free, no-obligation consultation with our Adaptiv experts. Let us guide you through a tailored solution that's just right for your unique needs.

Your journey to robust, reliable, and rapid application security begins now!

Talk To Us