Guide for running C# Azure Functions in an isolated worker process

This article is an introduction to working with .NET Functions isolated worker process, which runs your functions in an isolated worker process in Azure. This allows you to run your .NET class library functions on a version of .NET that is different from the version used by the Functions host process. For information about specific .NET versions supported, see supported version.

Use the following links to get started right away building .NET isolated worker process functions.

Getting started Concepts Samples

If you still need to run your functions in the same process as the host, see In-process C# class library functions.

For a comprehensive comparison between isolated worker process and in-process .NET Functions, see Differences between in-process and isolate worker process .NET Azure Functions.

Why .NET Functions isolated worker process?

When it was introduced, Azure Functions only supported a tightly integrated mode for .NET functions. In this in-process mode, your .NET class library functions run in the same process as the host. This mode provides deep integration between the host process and the functions. For example, when running in the same process .NET class library functions can share binding APIs and types. However, this integration also requires a tight coupling between the host process and the .NET function. For example, .NET functions running in-process are required to run on the same version of .NET as the Functions runtime. This means that your in-process functions can only run on version of .NET with Long Term Support (LTS). To enable you to run on non-LTS version of .NET, you can instead choose to run in an isolated worker process. This process isolation lets you develop functions that use current .NET releases not natively supported by the Functions runtime, including .NET Framework. Both isolated worker process and in-process C# class library functions run on LTS versions. To learn more, see Supported versions.

Because these functions run in a separate process, there are some feature and functionality differences between .NET isolated function apps and .NET class library function apps.

Benefits of isolated worker process

When your .NET functions run in an isolated worker process, you can take advantage of the following benefits:

  • Fewer conflicts: because the functions run in a separate process, assemblies used in your app won't conflict with different version of the same assemblies used by the host process.
  • Full control of the process: you control the start-up of the app and can control the configurations used and the middleware started.
  • Dependency injection: because you have full control of the process, you can use current .NET behaviors for dependency injection and incorporating middleware into your function app.

Supported versions

Versions of the Functions runtime work with specific versions of .NET. To learn more about Functions versions, see Azure Functions runtime versions overview. Version support depends on whether your functions run in-process or isolated worker process.

Note

To learn how to change the Functions runtime version used by your function app, see view and update the current runtime version.

The following table shows the highest level of .NET Core or .NET Framework that can be used with a specific version of Functions.

Functions runtime version In-process
(.NET class library)
Isolated worker process
(.NET Isolated)
Functions 4.x .NET 6.0 .NET 6.0
.NET 7.0 (GA)1
.NET Framework 4.8 (GA)1
Functions 3.x .NET Core 3.1
Functions 2.x .NET Core 2.12 n/a
Functions 1.x .NET Framework 4.8 n/a

1 Build process also requires .NET 6 SDK. Support for .NET Framework 4.8 is in GA.

2 For details, see Functions v2.x considerations.

For the latest news about Azure Functions releases, including the removal of specific older minor versions, monitor Azure App Service announcements.

.NET isolated worker process project

A .NET isolated function project is basically a .NET console app project that targets a supported .NET runtime. The following are the basic files required in any .NET isolated project:

For complete examples, see the .NET 6 isolated sample project and the .NET Framework 4.8 isolated sample project.

Note

To be able to publish your isolated function project to either a Windows or a Linux function app in Azure, you must set a value of dotnet-isolated in the remote FUNCTIONS_WORKER_RUNTIME application setting. To support zip deployment and running from the deployment package on Linux, you also need to update the linuxFxVersion site config setting to DOTNET-ISOLATED|7.0. To learn more, see Manual version updates on Linux.

Package references

A .NET Functions isolated worker process project uses a unique set of packages, for both core functionality and binding extensions.

Core packages

The following packages are required to run your .NET functions in an isolated worker process:

Extension packages

Because .NET isolated worker process functions use different binding types, they require a unique set of binding extension packages.

You'll find these extension packages under Microsoft.Azure.Functions.Worker.Extensions.

Start-up and configuration

When using .NET isolated functions, you have access to the start-up of your function app, which is usually in Program.cs. You're responsible for creating and starting your own host instance. As such, you also have direct access to the configuration pipeline for your app. With .NET Functions isolated worker process, you can much more easily add configurations, inject dependencies, and run your own middleware.

The following code shows an example of a HostBuilder pipeline:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(builder =>
    {
        builder
            .AddApplicationInsights()
            .AddApplicationInsightsLogger();
    })
    .ConfigureServices(s =>
    {
        s.AddSingleton<IHttpResponderService, DefaultHttpResponderService>();
    })
    .Build();

This code requires using Microsoft.Extensions.DependencyInjection;.

A HostBuilder is used to build and return a fully initialized IHost instance, which you run asynchronously to start your function app.

await host.RunAsync();

Important

If your project targets .NET Framework 4.8, you also need to add FunctionsDebugger.Enable(); before creating the HostBuilder. It should be the first line of your Main() method. For more information, see Debugging when targeting .NET Framework.

Configuration

The ConfigureFunctionsWorkerDefaults method is used to add the settings required for the function app to run in an isolated worker process, which includes the following functionality:

  • Default set of converters.
  • Set the default JsonSerializerOptions to ignore casing on property names.
  • Integrate with Azure Functions logging.
  • Output binding middleware and features.
  • Function execution middleware.
  • Default gRPC support.
.ConfigureFunctionsWorkerDefaults(builder =>
{
    builder
        .AddApplicationInsights()
        .AddApplicationInsightsLogger();
})

Having access to the host builder pipeline means that you can also set any app-specific configurations during initialization. You can call the ConfigureAppConfiguration method on HostBuilder one or more times to add the configurations required by your function app. To learn more about app configuration, see Configuration in ASP.NET Core.

These configurations apply to your function app running in a separate process. To make changes to the functions host or trigger and binding configuration, you'll still need to use the host.json file.

Dependency injection

Dependency injection is simplified, compared to .NET class libraries. Rather than having to create a startup class to register services, you just have to call ConfigureServices on the host builder and use the extension methods on IServiceCollection to inject specific services.

The following example injects a singleton service dependency:

.ConfigureServices(s =>
{
    s.AddSingleton<IHttpResponderService, DefaultHttpResponderService>();
})

This code requires using Microsoft.Extensions.DependencyInjection;. To learn more, see Dependency injection in ASP.NET Core.

Middleware

.NET isolated also supports middleware registration, again by using a model similar to what exists in ASP.NET. This model gives you the ability to inject logic into the invocation pipeline, and before and after functions execute.

The ConfigureFunctionsWorkerDefaults extension method has an overload that lets you register your own middleware, as you can see in the following example.

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerApplication =>
    {
        // Register our custom middlewares with the worker

        workerApplication.UseMiddleware<ExceptionHandlingMiddleware>();

        workerApplication.UseMiddleware<MyCustomMiddleware>();

        workerApplication.UseWhen<StampHttpHeaderMiddleware>((context) =>
        {
            // We want to use this middleware only for http trigger invocations.
            return context.FunctionDefinition.InputBindings.Values
                          .First(a => a.Type.EndsWith("Trigger")).Type == "httpTrigger";
        });
    })
    .Build();

The UseWhen extension method can be used to register a middleware that gets executed conditionally. You must pass to this method a predicate that returns a boolean value, and the middleware participates in the invocation processing pipeline when the return value of the predicate is true.

The following extension methods on FunctionContext make it easier to work with middleware in the isolated model.

Method Description
GetHttpRequestDataAsync Gets the HttpRequestData instance when called by an HTTP trigger. This method returns an instance of ValueTask<HttpRequestData?>, which is useful when you want to read message data, such as request headers and cookies.
GetHttpResponseData Gets the HttpResponseData instance when called by an HTTP trigger.
GetInvocationResult Gets an instance of InvocationResult, which represents the result of the current function execution. Use the Value property to get or set the value as needed.
GetOutputBindings Gets the output binding entries for the current function execution. Each entry in the result of this method is of type OutputBindingData. You can use the Value property to get or set the value as needed.
BindInputAsync Binds an input binding item for the requested BindingMetadata instance. For example, you can use this method when you have a function with a BlobInput input binding that needs to be accessed or updated by your middleware.

The following is an example of a middleware implementation that reads the HttpRequestData instance and updates the HttpResponseData instance during function execution. This middleware checks for the presence of a specific request header(x-correlationId), and when present uses the header value to stamp a response header. Otherwise, it generates a new GUID value and uses that for stamping the response header.

internal sealed class StampHttpHeaderMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        var requestData = await context.GetHttpRequestDataAsync();

        string correlationId;
        if (requestData!.Headers.TryGetValues("x-correlationId", out var values))
        {
            correlationId = values.First();
        }
        else
        {
            correlationId = Guid.NewGuid().ToString();
        }

        await next(context);

        context.GetHttpResponseData()?.Headers.Add("x-correlationId", correlationId);
    }
}

For a more complete example of using custom middleware in your function app, see the custom middleware reference sample.

Cancellation tokens

A function can accept a CancellationToken parameter, which enables the operating system to notify your code when the function is about to be terminated. You can use this notification to make sure the function doesn't terminate unexpectedly in a way that leaves data in an inconsistent state.

Cancellation tokens are supported in .NET functions when running in an isolated worker process. The following example raises an exception when a cancellation request has been received:

[Function(nameof(ThrowOnCancellation))]
public async Task ThrowOnCancellation(
    [EventHubTrigger("sample-workitem-1", Connection = "EventHubConnection")] string[] messages,
    FunctionContext context,
    CancellationToken cancellationToken)
{
    _logger.LogInformation("C# EventHub {functionName} trigger function processing a request.", nameof(ThrowOnCancellation));

    foreach (var message in messages)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await Task.Delay(6000); // task delay to simulate message processing
        _logger.LogInformation("Message '{msg}' was processed.", message);
    }
}

The following example performs clean-up actions if a cancellation request has been received:

[Function(nameof(HandleCancellationCleanup))]
public async Task HandleCancellationCleanup(
    [EventHubTrigger("sample-workitem-2", Connection = "EventHubConnection")] string[] messages,
    FunctionContext context,
    CancellationToken cancellationToken)
{
    _logger.LogInformation("C# EventHub {functionName} trigger function processing a request.", nameof(HandleCancellationCleanup));

    foreach (var message in messages)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            _logger.LogInformation("A cancellation token was received, taking precautionary actions.");
            // Take precautions like noting how far along you are with processing the batch
            _logger.LogInformation("Precautionary activities complete.");
            break;
        }

        await Task.Delay(6000); // task delay to simulate message processing
        _logger.LogInformation("Message '{msg}' was processed.", message);
    }
}

ReadyToRun

You can compile your function app as ReadyToRun binaries. ReadyToRun is a form of ahead-of-time compilation that can improve startup performance to help reduce the effect of cold-start when running in a Consumption plan.

ReadyToRun is available in .NET 3.1, .NET 6 (both in-process and isolated worker process), and .NET 7, and it requires version 3.0 or later of the Azure Functions runtime.

To compile your project as ReadyToRun, update your project file by adding the <PublishReadyToRun> and <RuntimeIdentifier> elements. The following is the configuration for publishing to a Windows 32-bit function app.

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  <RuntimeIdentifier>win-x86</RuntimeIdentifier>
  <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>

Execution context

.NET isolated passes a FunctionContext object to your function methods. This object lets you get an ILogger instance to write to the logs by calling the GetLogger method and supplying a categoryName string. To learn more, see Logging.

Bindings

Bindings are defined by using attributes on methods, parameters, and return types. A function method is a method with a Function attribute and a trigger attribute applied to an input parameter, as shown in the following example:

[Function("QueueFunction")]
[QueueOutput("output-queue")]
public static string[] Run([QueueTrigger("input-queue")] Book myQueueItem, FunctionContext context)

The trigger attribute specifies the trigger type and binds input data to a method parameter. The previous example function is triggered by a queue message, and the queue message is passed to the method in the myQueueItem parameter.

The Function attribute marks the method as a function entry point. The name must be unique within a project, start with a letter and only contain letters, numbers, _, and -, up to 127 characters in length. Project templates often create a method named Run, but the method name can be any valid C# method name.

Because .NET isolated projects run in a separate worker process, bindings can't take advantage of rich binding classes, such as ICollector<T>, IAsyncCollector<T>, and CloudBlockBlob. There's also no direct support for types inherited from underlying service SDKs, such as DocumentClient and BrokeredMessage. Instead, bindings rely on strings, arrays, and serializable types, such as plain old class objects (POCOs).

For HTTP triggers, you must 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.

For a complete set of reference samples for using triggers and bindings with isolated worker process functions, see the binding extensions reference sample.

Input bindings

A function can have zero or more input bindings that can pass data to a function. Like triggers, input bindings are defined by applying a binding attribute to an input parameter. When the function executes, the runtime tries to get data specified in the binding. The data being requested is often dependent on information provided by the trigger using binding parameters.

Output bindings

To write to an output binding, you must apply an output binding attribute to the function method, which defined how to write to the bound service. The value returned by the method is written to the output binding. For example, the following example writes a string value to a message queue named myqueue-output by using an output binding:

[Function("QueueFunction")]
[QueueOutput("output-queue")]
public static string[] Run([QueueTrigger("input-queue")] Book myQueueItem, FunctionContext context)
{
    // Use a string array to return more than one message.
    string[] messages = {
        $"Book name = {myQueueItem.Name}",
        $"Book ID = {myQueueItem.Id}"};
    var logger = context.GetLogger("QueueFunction");
    logger.LogInformation($"{messages[0]},{messages[1]}");

    // Queue Output messages
    return messages;
}

Multiple output bindings

The data written to an output binding is always the return value of the function. If you need to write to more than one output binding, you must create a custom return type. This return type must have the output binding attribute applied to one or more properties of the class. The following example from an HTTP trigger writes to both the HTTP response and a queue output binding:

public static class MultiOutput
{
    [Function("MultiOutput")]
    public static MyOutputType Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req,
        FunctionContext context)
    {
        var response = req.CreateResponse(HttpStatusCode.OK);
        response.WriteString("Success!");

        string myQueueOutput = "some output";

        return new MyOutputType()
        {
            Name = myQueueOutput,
            HttpResponse = response
        };
    }
}

public class MyOutputType
{
    [QueueOutput("myQueue")]
    public string Name { get; set; }

    public HttpResponseData HttpResponse { get; set; }
}

The response from an HTTP trigger is always considered an output, so a return value attribute isn't required.

HTTP trigger

HTTP triggers translates the incoming HTTP request message into an HttpRequestData object that is passed to the function. This object provides data from the request, including Headers, Cookies, Identities, URL, and optional a message Body. This object is a representation of the HTTP request object and not the request itself.

Likewise, the function returns an HttpResponseData object, which provides data used to create the HTTP response, including message StatusCode, Headers, and optionally a message Body.

The following code is an HTTP trigger

[Function("HttpFunction")]
public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req,
    FunctionContext executionContext)
{
    var logger = executionContext.GetLogger("HttpFunction");
    logger.LogInformation("message logged");

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Date", "Mon, 18 Jul 2016 16:06:00 GMT");
    response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

    response.WriteString("Welcome to .NET 5!!");

    return response;
}

Logging

In .NET isolated, you can write to logs by using an ILogger instance obtained from a FunctionContext object passed to your function. Call the GetLogger method, passing a string value that is the name for the category in which the logs are written. The category is usually the name of the specific function from which the logs are written. To learn more about categories, see the monitoring article.

The following example shows how to get an ILogger and write logs inside a function:

var logger = executionContext.GetLogger("HttpFunction");
logger.LogInformation("message logged");

Use various methods of ILogger to write various log levels, such as LogWarning or LogError. To learn more about log levels, see the monitoring article.

An ILogger is also provided when using dependency injection.

Debugging when targeting .NET Framework

If your isolated project targets .NET Framework 4.8, the current preview scope requires manual steps to enable debugging. These steps aren't required if using another target framework.

Your app should start with a call to FunctionsDebugger.Enable(); as its first operation. This occurs in the Main() method before initializing a HostBuilder. Your Program.cs file should look similar to the following:

using System;
using System.Diagnostics;
using Microsoft.Extensions.Hosting;
using Microsoft.Azure.Functions.Worker;
using NetFxWorker;

namespace MyDotnetFrameworkProject
{
    internal class Program
    {
        static void Main(string[] args)
        {
            FunctionsDebugger.Enable();

            var host = new HostBuilder()
                .ConfigureFunctionsWorkerDefaults()
                .Build();

            host.Run();
        }
    }
}

Next, you need to manually attach to the process using a .NET Framework debugger. Visual Studio doesn't do this automatically for isolated worker process .NET Framework apps yet, and the "Start Debugging" operation should be avoided.

In your project directory (or its build output directory), run:

func host start --dotnet-isolated-debug

This will start your worker, and the process will stop with the following message:

Azure Functions .NET Worker (PID: <process id>) initialized in debug mode. Waiting for debugger to attach...

Where <process id> is the ID for your worker process. You can now use Visual Studio to manually attach to the process. For instructions on this operation, see How to attach to a running process.

After the debugger is attached, the process execution resumes, and you'll be able to debug.

Remote Debugging using Visual Studio

Because your isolated worker process app runs outside the Functions runtime, you need to attach the remote debugger to a separate process. To learn more about debugging using Visual Studio, see Remote Debugging.

Next steps