Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
071dfdd
Initial plan
Copilot Dec 20, 2025
d4676ae
Refactor generator to avoid duplicate function definitions for class-…
Copilot Dec 20, 2025
adb3c4b
Address code review feedback
Copilot Dec 20, 2025
955a963
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 1, 2026
a3568bf
Update src/Generators/DurableTaskSourceGenerator.cs
YunchuWang Jan 2, 2026
288bfd7
Add version detection to prevent breaking changes for older Durable F…
Copilot Jan 2, 2026
341202f
Update version threshold to 1.11.0 for native class-based invocation …
Copilot Jan 2, 2026
0e256eb
update samples and tests
YunchuWang Jan 2, 2026
30dcc97
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 4, 2026
39279aa
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 5, 2026
1cd06bb
Fix smoke test CI failure by adding class-based task registration
Copilot Jan 5, 2026
39583d0
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 5, 2026
0e71f48
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 6, 2026
09cf08c
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 6, 2026
db10383
Fix ProjectTypeConfigurationTests for Durable Functions v1.11.0+ beha…
Copilot Jan 6, 2026
a1bd8dd
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 6, 2026
af70e7d
Merge branch 'main' into copilot/refactor-generator-for-durable-funct…
YunchuWang Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions samples/AzureFunctionsApp/AzureFunctionsApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<!-- Disable SDK's source generation to allow reflection-based discovery of source-generated functions -->
<FunctionsEnableExecutorSourceGen>false</FunctionsEnableExecutorSourceGen>
<FunctionsEnableWorkerIndexing>false</FunctionsEnableWorkerIndexing>
</PropertyGroup>

<ItemGroup>
Expand Down
20 changes: 20 additions & 0 deletions samples/AzureFunctionsApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using AzureFunctionsApp.Approval;
using AzureFunctionsApp.Entities;
using AzureFunctionsApp.Typed;
using Microsoft.DurableTask;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AzureFunctionsApp;

public class Program
Expand All @@ -10,6 +16,20 @@ public static void Main()
{
IHost host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.Configure<DurableTaskRegistry>(registry =>
{
registry
.AddOrchestrator<HelloCitiesTyped>()
.AddOrchestrator<ApprovalOrchestrator>()
.AddActivity<SayHelloTyped>()
.AddActivity<NotifyApprovalRequired>()
.AddEntity<Counter>()
.AddEntity<Lifetime>()
.AddEntity<UserEntity>();
});
})
.Build();

host.Run();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
Expand Down
84 changes: 73 additions & 11 deletions src/Generators/DurableTaskSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}

/// <summary>
/// Determines if code generation should be skipped for Durable Functions scenarios.
/// Returns true if only entities exist and the runtime supports native class-based invocation,
/// since entities don't generate extension methods and the runtime handles their registration.
/// </summary>
static bool ShouldSkipGenerationForDurableFunctions(
bool supportsNativeClassBasedInvocation,
List<DurableTaskTypeInfo> orchestrators,
List<DurableTaskTypeInfo> activities,
ImmutableArray<DurableEventTypeInfo> allEvents,
ImmutableArray<DurableFunction> allFunctions)
{
return supportsNativeClassBasedInvocation &&
orchestrators.Count == 0 &&
activities.Count == 0 &&
allEvents.Length == 0 &&
allFunctions.Length == 0;
}

/// Checks if a name is a valid C# identifier.
/// </summary>
/// <param name="name">The name to validate.</param>
Expand Down Expand Up @@ -322,6 +340,22 @@ static void Execute(
// Determine if we should generate Durable Functions specific code
bool isDurableFunctions = DetermineIsDurableFunctions(compilation, allFunctions, projectType);

// Check if the Durable Functions extension version supports native class-based invocation.
// This feature was introduced in PR #3229: https://github.com/Azure/azure-functions-durable-extension/pull/3229
// For the isolated worker extension (Microsoft.Azure.Functions.Worker.Extensions.DurableTask),
// native class-based invocation support was added in version 1.11.0.
bool supportsNativeClassBasedInvocation = false;
if (isDurableFunctions)
{
var durableFunctionsAssembly = compilation.ReferencedAssemblyNames.FirstOrDefault(
assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));

if (durableFunctionsAssembly != null && durableFunctionsAssembly.Version >= new Version(1, 11, 0))
{
supportsNativeClassBasedInvocation = true;
}
}

// Separate tasks into orchestrators, activities, and entities
// Skip tasks with invalid names to avoid generating invalid code
List<DurableTaskTypeInfo> orchestrators = new();
Expand Down Expand Up @@ -358,6 +392,16 @@ static void Execute(
return;
}

// With Durable Functions' native support for class-based invocations (PR #3229, v3.8.0+),
// we no longer generate [Function] definitions for class-based tasks when the runtime
// supports native invocation. If we have ONLY entities (no orchestrators, no activities,
// no events, no method-based functions), then there's nothing to generate for those
// scenarios since entities don't have extension methods.
if (ShouldSkipGenerationForDurableFunctions(supportsNativeClassBasedInvocation, orchestrators, activities, allEvents, allFunctions))
{
return;
}

StringBuilder sourceBuilder = new(capacity: found * 1024);
sourceBuilder.Append(@"// <auto-generated/>
#nullable enable
Expand All @@ -380,21 +424,34 @@ namespace Microsoft.DurableTask
{
public static class GeneratedDurableTaskExtensions
{");
if (isDurableFunctions)

// Generate singleton orchestrator instances for older Durable Functions versions
// that don't have native class-based invocation support
if (isDurableFunctions && !supportsNativeClassBasedInvocation)
{
// Generate a singleton orchestrator object instance that can be reused for all invocations.
foreach (DurableTaskTypeInfo orchestrator in orchestrators)
{
sourceBuilder.AppendLine($@"
static readonly ITaskOrchestrator singleton{orchestrator.TaskName} = new {orchestrator.TypeName}();");
}
}

// Note: When targeting Azure Functions (Durable Functions scenarios) with native support
// for class-based invocations (PR #3229, v3.8.0+), we no longer generate [Function] attribute
// definitions for class-based orchestrators, activities, and entities (i.e., classes that
// implement ITaskOrchestrator, ITaskActivity, or ITaskEntity and are decorated with the
// [DurableTask] attribute). The Durable Functions runtime handles function registration
// for these types automatically in those scenarios. For older versions of Durable Functions
// (prior to v3.8.0) or non-Durable Functions scenarios (for example, ASP.NET Core using
// the Durable Task Scheduler), we continue to generate [Function] definitions.
// We always generate extension methods for type-safe invocation.

foreach (DurableTaskTypeInfo orchestrator in orchestrators)
{
if (isDurableFunctions)
// Only generate [Function] definitions for Durable Functions if the runtime doesn't
// support native class-based invocation (versions prior to v3.8.0)
if (isDurableFunctions && !supportsNativeClassBasedInvocation)
{
// Generate the function definition required to trigger orchestrators in Azure Functions
AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator);
}

Expand All @@ -406,18 +463,20 @@ public static class GeneratedDurableTaskExtensions
{
AddActivityCallMethod(sourceBuilder, activity);

if (isDurableFunctions)
// Only generate [Function] definitions for Durable Functions if the runtime doesn't
// support native class-based invocation (versions prior to v3.8.0)
if (isDurableFunctions && !supportsNativeClassBasedInvocation)
{
// Generate the function definition required to trigger activities in Azure Functions
AddActivityFunctionDeclaration(sourceBuilder, activity);
}
}

foreach (DurableTaskTypeInfo entity in entities)
{
if (isDurableFunctions)
// Only generate [Function] definitions for Durable Functions if the runtime doesn't
// support native class-based invocation (versions prior to v3.8.0)
if (isDurableFunctions && !supportsNativeClassBasedInvocation)
{
// Generate the function definition required to trigger entities in Azure Functions
AddEntityFunctionDeclaration(sourceBuilder, entity);
}
}
Expand All @@ -437,16 +496,19 @@ public static class GeneratedDurableTaskExtensions
AddEventSendMethod(sourceBuilder, eventInfo);
}

if (isDurableFunctions)
// Note: The GeneratedActivityContext class is only needed for older versions of
// Durable Functions (prior to v3.8.0) that don't have native class-based invocation support.
// For v3.8.0+, the runtime handles class-based invocations natively.
if (isDurableFunctions && !supportsNativeClassBasedInvocation)
{
if (activities.Count > 0)
{
// Functions-specific helper class, which is only needed when
// using the class-based syntax.
// using the class-based syntax with older Durable Functions versions.
AddGeneratedActivityContextClass(sourceBuilder);
}
}
else
else if (!isDurableFunctions)
{
// ASP.NET Core-specific service registration methods
// Only generate if there are actually tasks to register
Expand Down
3 changes: 0 additions & 3 deletions test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
<!-- This is a smoke test application, not a unit test project -->
<IsTestProject>false</IsTestProject>
<IsPackable>false</IsPackable>
<!-- Disable SDK's source generation to allow reflection-based discovery of source-generated functions -->
<FunctionsEnableExecutorSourceGen>false</FunctionsEnableExecutorSourceGen>
<FunctionsEnableWorkerIndexing>false</FunctionsEnableWorkerIndexing>
</PropertyGroup>

<ItemGroup>
Expand Down
13 changes: 13 additions & 0 deletions test/AzureFunctionsSmokeTests/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.DurableTask;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AzureFunctionsSmokeTests;
Expand All @@ -11,6 +13,17 @@ public static void Main()
{
IHost host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.Configure<DurableTaskRegistry>(registry =>
{
registry
.AddOrchestrator<GeneratedOrchestration>()
.AddOrchestrator<ChildGeneratedOrchestration>()
.AddActivity<CountCharactersActivity>()
.AddEntity<GeneratorCounter>();
});
})
.Build();

host.Run();
Expand Down
Loading
Loading