.NET 7 Out-Of-Process Durable Functions, The beginning - Part 2

2022, Dec 07

The new .NET 7 Out-Of-Process (Isolated) Azure Durable Functions have a new interesting implementation. Class typed functions!

Basically we can define class-based Orchestrators and Activities, leveraging Object-Oriented Programming (OOP).

The Changes

The main change comes down to the Project file, in its TargetFramework and NuGet packages:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>DurableFunctionsNet7IsolatedClassBasedTypes</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.10.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.7.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.0.0-rc.1" />
    <PackageReference Include="Microsoft.DurableTask.Generators" Version="1.0.0-preview.1" OutputItemType="Analyzer" />
  </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>

The NuGet package Microsoft.Azure.Functions.Worker.Extensions.DurableTask is key here together with Microsoft.DurableTask.Generators, introducing the TaskOrchestrationContext, the context used on Orchestrator Functions, and the base classes for Orchestrators TaskOrchestrator and Activities TaskActivity. Class-based types are only possible with source generators, allowing for context extensions to be generated on the fly, and consumed in Orchestrators. These packages are not compatible with Durable Functions for the in-process .NET worker. It only works with the newer out-of-process .NET Isolated worker.

Have you heard about the Durable Task Framework before? This is explained on the Coding Night NZ YouTube video, but you can also check more details on the DTF Github.

It is worth mentioning that class-based types are likely to change, so it is better to continue with function-syntax for now.

The Example

This is a simple chaining pattern example, to demo the Isolated Durable Function. If you are not familiar with the Chaining pattern, check this article.

In this article, I'm not explaining the major components of Azure Durable Functions, but you can get more details about it in this article, or through the Coding Night NZ YouTube video.

This is a simple demo where the Starter exposes the HTTPTrigger endpoint, which calls the Orchestrator that in a chain Say Hello to the cities.

Starter Function (aka DurableClient)

[Function(nameof(StarterAsync))]
public async Task<HttpResponseData> StarterAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
    [DurableClient] DurableClientContext durableContext)
{
    _logger.LogInformation("C# HTTP trigger function processed a request.");

    string instanceId = await durableContext.Client.ScheduleNewOrchestrationInstanceAsync(nameof(SayHelloOrchestrator));

    _logger.LogInformation("Created new orchestration with instance ID = {instanceId}", instanceId);

    return durableContext.CreateCheckStatusResponse(req, instanceId);
}

This is an HTTPTrigger function that can be triggered with this CURL request:

curl --location --request GET 'http://localhost:7101/api/StarterAsync'

Orchestrator Function

[DurableTask(nameof(SayHelloOrchestrator))]
public class SayHelloOrchestrator : TaskOrchestrator<string?, string>
{
    public override async Task<string> RunAsync(TaskOrchestrationContext context, string? input)
    {
        string result = "";
        result += await context.CallSayHelloActivityAsync("Auckland") + " ";
        result += await context.CallSayHelloActivityAsync("London") + " ";
        result += await context.CallSayHelloActivityAsync("Seattle");
        return result;
    }
}

Activity Function

[DurableTask(nameof(SayHelloActivity))]
public class SayHelloActivity : TaskActivity<string, string>
{
    private readonly ILogger _logger;

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

    public override Task<string> RunAsync(TaskActivityContext context, string cityName)
    {
        _logger.LogInformation("Saying hello to {name}", cityName);

        return Task.FromResult($"Hello, {cityName}!");
    }
}

Saying hello is presented on output:

Output

Check the PlayGoKids repository for this article demo.

Full Series

.NET 7 Out-Of-Process Durable Functions, The beginning - Part 1

.NET 7 Out-Of-Process Durable Functions, The beginning - Part 2