Overview
Despite the fact that Azure Functions continues to evolve, its underlying technology also does. With the introduction of C# 12 and .NET 8 Isolated Process, developers face an exciting opportunity to enhance the performance, scalability, and maintainability of their serverless applications. Senior developers and tech leads will find detailed code examples and insights into migrating Azure Functions from older C# versions to C# 12 with .NET 8 in the Isolated Process model in this article.
Understand the .NET 8 Isolated Process Model
A .NET 8 Isolated Process model separates the Azure Functions runtime from the function code, which improves performance and simplifies dependency management. In place of the traditional Microsoft.NET.Sdk.Functions package, this model requires the use of Microsoft.Azure.Functions.Worker and Microsoft.Azure.Functions.Worker.Sdk packages.
Key Steps
- Update your project file to use Microsoft.Azure.Functions.Worker instead of Microsoft.NET.Sdk.Functions.
- Adjust your function app’s entry point to accommodate the new runtime model.
Example Project File Migration
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.20.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.16.4" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AzureFunctionsApp.Shared\AzureFunctionsApp.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
</ItemGroup>
</Project>
Upgrade Code to Leverage C# 12 Features
It is recommended that you consider adopting the following features in C# 12 where they are applicable, in order to simplify code and improve performance:
Constructor parameter handling and initialization should be simplified for primary constructors.
Before C# 12
namespace AzureFunctionsApp.Shared.Models
{
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public string EmailAddress { get; }
public string TelphoneNumber { get; }
public Person(string firstName, string lastName, string eamilAddress, string telephoneNumber)
{
FirstName = firstName;
LastName = lastName;
EmailAddress = eamilAddress;
TelphoneNumber = telephoneNumber;
}
}
}
After C# 12
namespace AzureFunctionsApp.Shared.Models
public readonly record struct Person(string FirstName, string LastName, string EmailAddress, string TelephoneNumber);
Record Structs: Immutable data structures that provide better performance and memory efficiency.
Example
public readonly record struct Person(string FirstName, string LastName, string EmailAddress, string TelephoneNumber);
Refactor to Asynchronous Programming Model
The Isolated Process model promotes asynchronous operations to avoid blocking threads and improve scalability. Ensure all I/O-bound operations are asynchronous and utilize async and await keywords properly.
Example of Before
using AzureFunctionsApp.Shared.Services.Interfaces;
namespace AzureFunctionsApp.Shared.Services;
public class DataService
{
public async Task<string> GetData()
{
var data = await SomeLongRunningOperation();
await DoAnotherAsyncOperation();
return data;
}
public async Task<string> SomeLongRunningOperation()
{
await Task.Delay(1000);
return "Data from long-running operation";
}
public async Task DoAnotherAsyncOperation()
{
await Task.Delay(500);
}
}
Example of After
It is important to ensure that all method calls that support asynchronous operations are awaited correctly.
IDataService.cs:
namespace AzureFunctionsApp.Shared.Services.Interfaces;
public interface IDataService
{
string GetData();
Task<string> GetDataAsync();
Task<string> SomeLongRunningOperationAsync();
Task DoAnotherAsyncOperationAsync();
}
DataService.cs:
using AzureFunctionsApp.Shared.Services.Interfaces;
namespace AzureFunctionsApp.Shared.Services;
public class DataService: IDataService
{
public string GetData()
{
return "Hello from Ziggy Rafiq!";
}
public async Task<string> GetDataAsync()
{
var data = await SomeLongRunningOperationAsync();
await DoAnotherAsyncOperationAsync();
return data;
}
public async Task<string> SomeLongRunningOperationAsync()
{
await Task.Delay(1000);
return "Long-running operation result";
}
public async Task DoAnotherAsyncOperationAsync()
{
await Task.Delay(500);
}
}
Adapt Dependency Injection and Configuration
For the isolated model, dependency injection (DI) and configuration management are handled differently. IServiceCollection is used to configure services and ConfigurationBuilder is used to configure application settings.
using AzureFunctionsApp.Shared.Services;
using AzureFunctionsApp.Shared.Services.Interfaces;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
services.AddSingleton<IDataService, DataService>();
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
})
.Build();
host. Run();
Configuration Setup
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AzureFunctionsApp;
public class ZiggyFunction
{
private readonly ILogger<ZiggyFunction> _logger;
private readonly IConfiguration _configuration;
public ZiggyFunction(ILogger<ZiggyFunction> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
[Function(nameof(ZiggyFunction))]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
{
string? ziggyAppSettingConfiguration = string.IsNullOrWhiteSpace(_configuration["ZiggyAppSetting"]) ? "ZiggyDefaultValue": _configuration["ZiggyAppSetting"];
_logger.LogInformation("Hello from Ziggy Rafiq");
return new OkObjectResult("Welcome to Ziggy Azure Functions!");
}
}
Summary
Azure Functions users can benefit from improved performance and new language features when they migrate to C# 12 with .NET 8 Isolated Process. This updated environment can be effectively transitioned by understanding the new model, leveraging modern C# features, and adapting your code for asynchronous programming and DI. It is imperative to test and validate your serverless applications to ensure a smooth migration. Adopt these practices to enhance their efficiency and scalability.
You can take advantage of the latest advancements in Azure Functions and .NET by following these best practices.
For the complete source code for the examples in this article, please visit my GitHub repository. I'd appreciate your support if you liked this article and followed me on LinkedIn.