Unveiling .NET 8 – Part 2: Seamless Azure Functions Integration

Introduction

In the second and final installment of this two-part series, we’ll be changing aim from the features made available by .NET 8, and focusing on the latest changes made available to Azure Functions at the release of .NET 8.

Overview

Azure Functions offers two models for building function apps: the in-process model and the isolated model. In the past, the isolated model was more difficult to use due to a lack of proper binding support when compared to the in-process model. However, the latest update has brought complete feature parity between the two, meaning that in the isolated model, the input / output / trigger bindings and the model types exposed by them are finally equivalent to what we see in the in-process model.

The main difference between the two models lies in how they operate. The in-process model runs functions within the same process as the host, coupling the user’s code as they must run the exact same library / .NET versions as the host. On the other hand, the isolated model runs functions in a separate, isolated process, offering flexibility, control, and compatibility with a broader range of .NET versions, including .NET Framework.

From the time between the last roadmap update and the latest roadmap update, the isolated Azure Function model has added the following support:

  • Support for .NET Framework 4.8
  • Support for .NET 7 / .NET 8
  • Support for Durable Functions
  • Support for Application Insights from the worker
  • Support for binding to SDK types
  • Preview support for ASP.NET Core integration

Microsoft’s clear preference for the isolated model signals a forward-looking approach. This shift is driven by the need for greater flexibility and an overall decoupling of the host process and the application process itself: The coupling was useful initially, as it allowed for bindings to be created as the in-process model’s libraries could rely on types and assemblies being the same across the host and the application code, however as the isolated model has evolved and finally built out feature parity, it’s time to get rid of that coupling. Microsoft has commented that .NET 8 will be the last LTS release to receive in-process model support in Azure Functions, and even so, is not available on release, and will come later next year.

The Azure Functions Road Map

Upgrading

  1. Ensure you are running an isolated Azure Function. For assistance in migrating from the in-process model to the isolated model, visit the migration guide.
  2. Locate all the relevant `.csproj` files and update the target framework to `net8.0` and the Azure Functions Version to `v4`.
  3. Analyse whether or not you can benefit from adding any new packages, such as the newly available ASP.NET Core extensions or Application Insights extensions.
  4. Update all other Azure Functions related packages to their latest versions.
  5. With that all done, you should be free to take advantage of any of the new .NET 8 / C# 12 / Azure Functions features straight away!

Alternatively, feel free to use the .NET upgrade assistant, which should be targeting .NET 8 as the recommended default for Azure Functions going forward.

Changes

Rich Bindings

The isolated model has come a long way, with rich-type support for bindings working out of the box where in the past we were limited to cobbling together data from strings or the `FunctionContext` object.

A popular example now made possible is getting a handle to a BlobClient via attributes. In the past, support for this functionality only existed in the in-process model, however now through input / output / trigger attributes, you can have access to a BlobClient at the start of the function invocation.

Below is probably the most evident example of this, which showcases the old approach to extracting metadata from an Event Hub triggered function, and the new approach below it:

# Old
[Function("EventDataBatchFunction")]
[FixedDelayRetry(5, "00:00:10")]
public static void UsingParameters([EventHubTrigger("src-parameters", Connection = "EventHubConnectionAppSetting")] string[] messages,
    DateTime[] enqueuedTimeUtcArray,
    long[] sequenceNumberArray,
    string[] offsetArray,
    Dictionary<string, JsonElement>[] propertiesArray,
    Dictionary<string, JsonElement>[] systemPropertiesArray)
{
    // You can directly access binding data via parameters.
    for (int i = 0; i < messages.Length; i++)
    {
        string message = messages[i];
        DateTime enqueuedTimeUtc = enqueuedTimeUtcArray[i];
        long sequenceNumber = sequenceNumberArray[i];
        string offset = offsetArray[i];

        // Note: The values in these dictionaries are sent to the worker as JSON. By default, System.Text.Json will not automatically infer primitive values
        //       if you attempt to deserialize to 'object'. See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0#deserialization-of-object-properties       
        //
        //       If you want to use Dictionary<string, object> and have the values automatically inferred, you can use the sample JsonConverter specified
        //       here: https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0#deserialize-inferred-types-to-object-properties
        //
        //       See the Configuration sample in this repo for details on how to add this custom converter to the JsonSerializerOptions, or for 
        //       details on how to use Newtonsoft.Json, which does automatically infer primitive values.
        Dictionary<string, JsonElement> properties = propertiesArray[i];
        Dictionary<string, JsonElement> systemProperties = systemPropertiesArray[i];
    }
}

# New
[Function(nameof(EventDataBatchFunction))]
public void EventDataBatchFunction(
    [EventHubTrigger("queue", Connection = "EventHubConnection")] EventData[] events)
{
    foreach (EventData @event in events)
    {
        _logger.LogInformation("Event Body: {body}", @event.Body);
        _logger.LogInformation("Event Content-Type: {contentType}", @event.ContentType);
    }
}

Application Insights Integration

Microsoft has made significant improvements to the integration of Application Insights within isolated function apps, allowing developers to have more control over telemetry collection by allowing the configuration of custom filtering rules, log levels, and more within the host builder.

Firstly, add the following NuGet packages:

dotnet add package Microsoft.ApplicationInsights.WorkerService
dotnet add package Microsoft.Azure.Functions.Worker.ApplicationInsights

Secondly, some changes are required for the HostBuilder: Adjust your ConfigureServices to configure Application Insights for Azure Functions accordingly and any potential tweaks you may want to make in the ConfigureLogging call. A minimal example can be seen below:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services => {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .ConfigureLogging(logging =>
    {
        logging.Services.Configure<LoggerFilterOptions>(options =>
        {
            // example
            LoggerFilterRule defaultRule = options.Rules.FirstOrDefault(rule => rule.ProviderName
                == "Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider");
            if (defaultRule is not null)
            {
                options.Rules.Remove(defaultRule);
            }
        });
    })
    .Build();

host.Run();

ASP.NET Core Integration

Typically, Azure Functions HTTP triggers utilize a built-in HTTP model that exposes an HttpRequestData object that includes all the context of a given request and response, such as headers, body data, query parameters, response data, and so forth. This differs from the usual model that ASP developers are used to, which is dealing with IActionsResults, HttpRequest and HttpResponse. These features are now available as an integration for Isolated Azure Functions via NuGet package and some minor configurations.

Firstly, add the following NuGet package:

dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore

Secondly, some changes are required for the HostBuilder: The call to ConfigureFunctionsWorkerDefault must be replaced with ConfigureFunctionsWebApplication. A minimal example can be seen below:

using Microsoft.Extensions.Hosting;
using Microsoft.Azure.Functions.Worker;

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

host.Run();

Finally, your HTTP triggers can be adjusted to utilize the aforementioned types:

[Function("HttpFunction")]
public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
    return new OkObjectResult($"Welcome to Azure Functions, {req.Query["name"]}!");
}

Caveats

As always, there are some caveats with this latest update, mainly in terms of the Azure Functions integrations:

  • You cannot use .NET 8s “Ahead of Time” compilation due to internal Azure Function libraries depending on and using System.Text.Json without source generation. You can, however, use “Ready to Run” which is similar. This seems to be a tracked GitHub issue, indicating that the team is actively working on enabling AoT support for Azure Functions, which would provide a large boost in cold start times whilst maintaining compatibility across different systems and architectures.
  • The new “KeyedSingleton” dependency injection method is seemingly missing from the current Azure Function libraries. This seems to be due to the fact that the libraries offered by the Azure Functions run-time may be pinning an earlier version of the DotNet dependency injection extensions library.

Conclusion

The release of .NET 8 brings a host of exciting features and performance enhancements for developers. With the introduction of C# 12, the language itself is evolving to become more powerful and expressive. Whether you’re looking forward to primary constructors, collection expressions, or leveraging the new performance-focused types, there’s a lot to explore and benefit from in this release.

In the world of Azure Functions, the isolated model has evolved to offer feature parity with the in-process model, providing developers with more flexibility and control over their processes.

So, whether you’re a .NET developer working on C# code, or a cloud-native developer building out Azure Functions, it’s an exciting time to explore what .NET has to offer!

Read more recent blogs

Get started on the right path to cloud success today. Our Crew are standing by to answer your questions and get you up and running.