Back to top

Upgrading Azure Functions to .NET 7 Isolated Worker Process

I’ve been working this weekend on changing some Azure Functions from .NET 6 using the “in-process” mode, to .NET 7 using the “isolated worker process” mode. Do I know how to make the most of glorious weather?? I sure do!

It hasn’t been an exactly smooth process for me, and I’m going to be honest, I am still not 100% understanding the differences here, but the bottom line is that from .NET 7 we can no longer use the in-process mode. I actually struggled to find any clear guides on “How to migrate Azure Functions from in-process to isolated worker process”, I could only find tids and bits around the place. A lot of what I worked out came from teasing out the details in this guide from Microsoft on isolated Worker processes into my own requirements.

Here are some key learnings from my experiences.

Upgrade your Visual Studio

Make sure you’re running the latest version of Visual Studio 2022. I thought I was on 17.4, which I remembered seeing somewhere was required for all this to work, but it turned out I wasn’t. Be sure you’re on the latest version before going through all this!

Update your Azure Functions tools in Visual Studio

I did notice that even with the latest version of VS, I was still not seeing any options for .NET 7 Isolated Worker Process when creating a new project. It turned out, the Azure Functions Toolsets and Templates weren’t updated automatically.

Go to Tools menu > Options > Project & Solutions > Azure Functions and use that lovely big button to check / install updates!

alt text

Target .NET 7

To target .NET 7, just change the .csproj file to have the new TargetFramework:

<TargetFramework>net7.0</TargetFramework>

You’ll also need to change the output type to executable, which can be achieved by adding this line to your .csproj file:

<OutputType>Exe</OutputType>

Add/Update packages

Add some new core packages, as per the abovementioned guide:

  • Microsoft.Azure.Functions.Worker
  • Microsoft.Azure.Functions.Worker.Sdk

I had a reference to the old Microsoft.NET.Sdk.Functions package, which I was able to remove, too.

You’ll also need to change from using the Microsoft.Extensions.* packages to using Microsoft.Azure.Functions.Worker.Extensions.*. For example Microsoft.Azure.Functions.Worker.Extensions.Http, and Microsoft.Azure.Functions.Worker.Extensions.Timer (I had to actually add the latter, I hadn’t needed anything explicitly for timers before).

Create or Update your Program.cs

As outlined in the above guide, you need to update your code to have a Program.cs for entry, and the following is what I used, based on the above guide:

var host = new HostBuilder()
  .ConfigureFunctionsWorkerDefaults(builder =>
  {
    // Nb: I couldn't get the following to work from the guide, I think Application Insights will "just work" now, anyway?
    //builder
    //    .AddApplicationInsights()
    //    .AddApplicationInsightsLogger();
  })
  .ConfigureServices(s =>
  {
    // Add any service registration, e.g.
    // service.AddScoped<IMyInterface, MyClass>();
  })
  .Build();

await host.RunAsync();

Change the [FunctionName] attribute

Update: I previously got caught up in changing the parameter to the attribute to use nameof(ClassName), and just now realised that of course it can still just take a string, which is all nameof() will be doing. So I’m updating the code below, and that lovely regular expression is no longer needed. I’ll leave it there for fun, anyway, and plus as the lovely Aaron Powell has just pointed out to me, it is better to use nameof() to help enforce function names being unique, anyway.

I just spent the last half hour going a litle bit greyer, as I was getting the following error when I actually tried to run the functions on my dev machine:

alt text

No job functions found. Try making your job classes and methods public. If you’re using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you’ve called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).

The majority of that warning was a total bum-steer for me! It turns out that the attribute for naming the functions needs to change from FunctionName to Function.

Previously I had:

[FunctionName("MyFunctionName")]
public async Task Run(
  [TimerTrigger("0 */5 * * * *")] TimerInfo myTimer,
  ILogger logger)
{
  ...
}

This FunctionName attribute needs to change to:

[Function("MyFunctionName")]
public async Task Run(
  [TimerTrigger("0 */5 * * * *")] TimerInfo myTimer,
  ILogger logger)
{
  ...
}

As per my update above, there was no need to change the code to use nameof() after all, but for fun and future reference, here’s a helpful regex for Find and Replace if you wanted to change over to that way of passing the function name in (which I’m going to stick with in my own code)!

Find:

\[FunctionName\("(.*)"\)\]

Replace with:

[Function(nameof($1))]

Changes to the function signatures

As per the guide, for HTTP triggers we no longer accept HttpRequest, instead we need to use HttpRequestData and HttpResponseData to access the request and response data. This is because you don’t have access to the original HTTP request and response objects when using .NET Functions isolated worker process.

In one function, I was getting certain query parameters out from the request, which I was previously able to do like this:

var querySomeString = req.Query["somestring"];

In the new world, I needed to change the input parameter req to be of type HttpRequestData, and use the following to get the query strings:

var queryString = System.Web.HttpUtility.ParseQueryString(req.Url.Query);

var querySomeString = queryString["somestring"];

Changes to logging

Rather than passing ILogger in via the funciton parameters like this:

public async Task Run(
  [TimerTrigger("0 0 * * * *")] TimerInfo myTimer,
  ILogger logger)

we instead need to access the logger either via the execution context:

var logger = executionContext.GetLogger("HttpFunction");

or using dependancy injection, such as like this:

private readonly ILogger _logger;

public MyFunction(ILoggerFactory loggerFactory)
{
  _logger = loggerFactory.CreateLogger<MyFunction>();
}

One last little thing - binding updates

For my scenario, I was using input and output bindings to read and write data to a CosmosDB, including one case where I was doing multiple output bindings. As part of this upgrade, I had to change the way I did multiple output bindings, and I’ve written about how to do multiple output bindings for CosmosDB in isolated worker process Azure Functions in another post.

And with that, finally my Azure Functions are running!

Phew!