From 071dfdd5e104801c96a4e38d44e00a69982eb8ea Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 20 Dec 2025 00:34:13 +0000
Subject: [PATCH 1/9] Initial plan
From d4676ae2bc44c45a4fa31a1f688e2b021fb6dbaf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 20 Dec 2025 00:45:42 +0000
Subject: [PATCH 2/9] Refactor generator to avoid duplicate function
definitions for class-based tasks
- Remove generation of [Function] attribute definitions for class-based orchestrators, activities, and entities
- Keep generating extension methods for type-safe invocation
- Add early return when only entities exist in Durable Functions scenarios
- Update tests to reflect new behavior where Durable Functions natively handles class-based invocations
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 63 ++++-----
test/Generators.Tests/AzureFunctionsTests.cs | 129 ++++---------------
test/Generators.Tests/Utils/TestHelpers.cs | 13 +-
3 files changed, 56 insertions(+), 149 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 0b4e717e..c8185a6e 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -274,6 +274,20 @@ static void Execute(
return;
}
+ // With Durable Functions' native support for class-based invocations (PR #3229),
+ // we no longer generate [Function] definitions for class-based tasks.
+ // If we have ONLY entities (no orchestrators, no activities, no events, no method-based functions),
+ // then there's nothing to generate for Durable Functions scenarios.
+ if (isDurableFunctions &&
+ orchestrators.Count == 0 &&
+ activities.Count == 0 &&
+ allEvents.Length == 0 &&
+ allFunctions.Length == 0)
+ {
+ // Only entities remain, and entities don't generate extension methods
+ return;
+ }
+
StringBuilder sourceBuilder = new(capacity: found * 1024);
sourceBuilder.Append(@"//
#nullable enable
@@ -296,24 +310,14 @@ namespace Microsoft.DurableTask
{
public static class GeneratedDurableTaskExtensions
{");
- if (isDurableFunctions)
- {
- // 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: With Durable Functions' native support for class-based invocations (PR #3229),
+ // we no longer generate [Function] definitions for class-based tasks to avoid duplicates.
+ // The Durable Functions runtime now handles this automatically.
+ // We continue to generate extension methods for type-safe invocation.
foreach (DurableTaskTypeInfo orchestrator in orchestrators)
{
- if (isDurableFunctions)
- {
- // Generate the function definition required to trigger orchestrators in Azure Functions
- AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator);
- }
-
AddOrchestratorCallMethod(sourceBuilder, orchestrator);
AddSubOrchestratorCallMethod(sourceBuilder, orchestrator);
}
@@ -321,22 +325,9 @@ public static class GeneratedDurableTaskExtensions
foreach (DurableTaskTypeInfo activity in activities)
{
AddActivityCallMethod(sourceBuilder, activity);
-
- if (isDurableFunctions)
- {
- // Generate the function definition required to trigger activities in Azure Functions
- AddActivityFunctionDeclaration(sourceBuilder, activity);
- }
}
- foreach (DurableTaskTypeInfo entity in entities)
- {
- if (isDurableFunctions)
- {
- // Generate the function definition required to trigger entities in Azure Functions
- AddEntityFunctionDeclaration(sourceBuilder, entity);
- }
- }
+ // Entities don't have extension methods, so no generation needed for them
// Activity function triggers are supported for code-gen (but not orchestration triggers)
IEnumerable activityTriggers = allFunctions.Where(
@@ -353,16 +344,10 @@ public static class GeneratedDurableTaskExtensions
AddEventSendMethod(sourceBuilder, eventInfo);
}
- if (isDurableFunctions)
- {
- if (activities.Count > 0)
- {
- // Functions-specific helper class, which is only needed when
- // using the class-based syntax.
- AddGeneratedActivityContextClass(sourceBuilder);
- }
- }
- else
+ // Note: GeneratedActivityContext is no longer needed since Durable Functions
+ // now natively handles class-based invocations without source generation.
+
+ if (!isDurableFunctions)
{
// ASP.NET Core-specific service registration methods
// Only generate if there are actually tasks to register
diff --git a/test/Generators.Tests/AzureFunctionsTests.cs b/test/Generators.Tests/AzureFunctionsTests.cs
index d9d7fad0..5454752f 100644
--- a/test/Generators.Tests/AzureFunctionsTests.cs
+++ b/test/Generators.Tests/AzureFunctionsTests.cs
@@ -119,7 +119,8 @@ await TestHelpers.RunTestAsync(
///
/// Verifies that using the class-based activity syntax generates a
- /// extension method as well as an function definition.
+ /// extension method. With PR #3229, Durable Functions now natively handles class-based invocations,
+ /// so the generator no longer creates [Function] attribute definitions to avoid duplicates.
///
/// The activity input type.
/// The activity output type.
@@ -143,13 +144,6 @@ public class MyActivity : TaskActivity<{inputType}, {outputType}>
public override Task<{outputType}> RunAsync(TaskActivityContext context, {inputType} input) => Task.FromResult<{outputType}>(default!);
}}";
- // Build the expected InputParameter format (matches generator logic)
- string expectedInputParameter = inputType + " input";
- if (inputType.EndsWith('?'))
- {
- expectedInputParameter += " = default";
- }
-
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: $@"
@@ -160,17 +154,7 @@ public class MyActivity : TaskActivity<{inputType}, {outputType}>
public static Task<{outputType}> CallMyActivityAsync(this TaskOrchestrationContext ctx, {inputType} input, TaskOptions? options = null)
{{
return ctx.CallActivityAsync<{outputType}>(""MyActivity"", input, options);
-}}
-
-[Function(nameof(MyActivity))]
-public static async Task<{outputType}> MyActivity([ActivityTrigger] {expectedInputParameter}, string instanceId, FunctionContext executionContext)
-{{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
- TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
- object? result = await activity.RunAsync(context, input);
- return ({outputType})result!;
-}}
-{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
+}}",
isDurableFunctions: true);
await TestHelpers.RunTestAsync(
@@ -183,7 +167,8 @@ await TestHelpers.RunTestAsync(
///
/// Verifies that using the class-based syntax for authoring orchestrations generates
/// type-safe and
- /// extension methods as well as function triggers.
+ /// extension methods. With PR #3229, Durable Functions now natively handles class-based
+ /// invocations, so the generator no longer creates [Function] attribute definitions.
///
/// The activity input type.
/// The activity output type.
@@ -221,15 +206,6 @@ public class MyOrchestrator : TaskOrchestrator<{inputType}, {outputType}>
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: $@"
-static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
-
-[Function(nameof(MyOrchestrator))]
-public static Task<{outputType}> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
-{{
- return singletonMyOrchestrator.RunAsync(context, context.GetInput<{inputType}>())
- .ContinueWith(t => ({outputType})(t.Result ?? default({outputType})!), TaskContinuationOptions.ExecuteSynchronously);
-}}
-
///
/// Schedules a new instance of the orchestrator.
///
@@ -261,7 +237,8 @@ await TestHelpers.RunTestAsync(
///
/// Verifies that using the class-based syntax for authoring orchestrations generates
/// type-safe and
- /// extension methods as well as function triggers.
+ /// extension methods. With PR #3229, Durable Functions now natively handles class-based
+ /// invocations, so the generator no longer creates [Function] attribute definitions.
///
/// The activity input type.
/// The activity output type.
@@ -304,15 +281,6 @@ public abstract class MyOrchestratorBase : TaskOrchestrator<{inputType}, {output
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: $@"
-static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
-
-[Function(nameof(MyOrchestrator))]
-public static Task<{outputType}> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
-{{
- return singletonMyOrchestrator.RunAsync(context, context.GetInput<{inputType}>())
- .ContinueWith(t => ({outputType})(t.Result ?? default({outputType})!), TaskContinuationOptions.ExecuteSynchronously);
-}}
-
///
/// Schedules a new instance of the orchestrator.
///
@@ -342,8 +310,9 @@ await TestHelpers.RunTestAsync(
}
///
- /// Verifies that using the class-based syntax for authoring entities generates
- /// function triggers for Azure Functions.
+ /// Verifies that using the class-based syntax for authoring entities no longer generates
+ /// any code for Azure Functions. With PR #3229, Durable Functions now natively handles
+ /// class-based invocations. Entities don't have extension methods, so nothing is generated.
///
/// The entity state type.
[Theory]
@@ -366,26 +335,17 @@ public class MyEntity : TaskEntity<{stateType}>
}}
}}";
- string expectedOutput = TestHelpers.WrapAndFormat(
- GeneratedClassName,
- methodList: @"
-[Function(nameof(MyEntity))]
-public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
-{
- return dispatcher.DispatchAsync();
-}",
- isDurableFunctions: true);
-
+ // With PR #3229, no code is generated for class-based entities in Durable Functions
await TestHelpers.RunTestAsync(
GeneratedFileName,
code,
- expectedOutput,
+ expectedOutputSource: null, // No output expected
isDurableFunctions: true);
}
///
- /// Verifies that using the class-based syntax for authoring entities with inheritance generates
- /// function triggers for Azure Functions.
+ /// Verifies that using the class-based syntax for authoring entities with inheritance no longer generates
+ /// any code for Azure Functions. With PR #3229, Durable Functions now natively handles class-based invocations.
///
/// The entity state type.
[Theory]
@@ -413,26 +373,17 @@ public abstract class MyEntityBase : TaskEntity<{stateType}>
}}
}}";
- string expectedOutput = TestHelpers.WrapAndFormat(
- GeneratedClassName,
- methodList: @"
-[Function(nameof(MyEntity))]
-public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
-{
- return dispatcher.DispatchAsync();
-}",
- isDurableFunctions: true);
-
+ // With PR #3229, no code is generated for class-based entities in Durable Functions
await TestHelpers.RunTestAsync(
GeneratedFileName,
code,
- expectedOutput,
+ expectedOutputSource: null, // No output expected
isDurableFunctions: true);
}
///
- /// Verifies that using the class-based syntax for authoring entities with custom state types generates
- /// function triggers for Azure Functions.
+ /// Verifies that using the class-based syntax for authoring entities with custom state types no longer generates
+ /// any code for Azure Functions. With PR #3229, Durable Functions now natively handles class-based invocations.
///
[Fact]
public async Task Entities_ClassBasedSyntax_CustomStateType()
@@ -457,26 +408,19 @@ public class MyEntity : TaskEntity
}
}";
- string expectedOutput = TestHelpers.WrapAndFormat(
- GeneratedClassName,
- methodList: @"
-[Function(nameof(MyEntity))]
-public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
-{
- return dispatcher.DispatchAsync();
-}",
- isDurableFunctions: true);
-
+ // With PR #3229, no code is generated for class-based entities in Durable Functions
await TestHelpers.RunTestAsync(
GeneratedFileName,
code,
- expectedOutput,
+ expectedOutputSource: null, // No output expected
isDurableFunctions: true);
}
///
/// Verifies that using the class-based syntax for authoring a mix of orchestrators, activities,
- /// and entities generates the appropriate function triggers for Azure Functions.
+ /// and entities generates the appropriate extension methods for Azure Functions.
+ /// With PR #3229, Durable Functions now natively handles class-based invocations,
+ /// so the generator no longer creates [Function] attribute definitions.
///
[Fact]
public async Task Mixed_OrchestratorActivityEntity_ClassBasedSyntax()
@@ -512,15 +456,6 @@ public class MyEntity : TaskEntity
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: $@"
-static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
-
-[Function(nameof(MyOrchestrator))]
-public static Task MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
-{{
- return singletonMyOrchestrator.RunAsync(context, context.GetInput())
- .ContinueWith(t => (string)(t.Result ?? default(string)!), TaskContinuationOptions.ExecuteSynchronously);
-}}
-
///
/// Schedules a new instance of the orchestrator.
///
@@ -548,23 +483,7 @@ public static Task CallMyOrchestratorAsync(
public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
{{
return ctx.CallActivityAsync(""MyActivity"", input, options);
-}}
-
-[Function(nameof(MyActivity))]
-public static async Task MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
-{{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
- TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
- object? result = await activity.RunAsync(context, input);
- return (string)result!;
-}}
-
-[Function(nameof(MyEntity))]
-public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
-{{
- return dispatcher.DispatchAsync();
-}}
-{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
+}}",
isDurableFunctions: true);
await TestHelpers.RunTestAsync(
diff --git a/test/Generators.Tests/Utils/TestHelpers.cs b/test/Generators.Tests/Utils/TestHelpers.cs
index 960a525f..743c3f02 100644
--- a/test/Generators.Tests/Utils/TestHelpers.cs
+++ b/test/Generators.Tests/Utils/TestHelpers.cs
@@ -15,7 +15,7 @@ static class TestHelpers
public static Task RunTestAsync(
string expectedFileName,
string inputSource,
- string expectedOutputSource,
+ string? expectedOutputSource,
bool isDurableFunctions) where TSourceGenerator : IIncrementalGenerator, new()
{
CSharpSourceGeneratorVerifier.Test test = new()
@@ -23,10 +23,6 @@ public static Task RunTestAsync(
TestState =
{
Sources = { inputSource },
- GeneratedSources =
- {
- (typeof(TSourceGenerator), expectedFileName, SourceText.From(expectedOutputSource, Encoding.UTF8, SourceHashAlgorithm.Sha256)),
- },
AdditionalReferences =
{
// Durable Task SDK
@@ -35,6 +31,13 @@ public static Task RunTestAsync(
},
};
+ // Only add generated source if expectedOutputSource is not null
+ if (expectedOutputSource != null)
+ {
+ test.TestState.GeneratedSources.Add(
+ (typeof(TSourceGenerator), expectedFileName, SourceText.From(expectedOutputSource, Encoding.UTF8, SourceHashAlgorithm.Sha256)));
+ }
+
if (isDurableFunctions)
{
// Durable Functions code generation is triggered by the presence of the
From adb3c4bd88d355eee3d620cfdffb0c6dde84c715 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 20 Dec 2025 00:52:25 +0000
Subject: [PATCH 3/9] Address code review feedback
- Extract entity-only check into ShouldSkipGenerationForDurableFunctions helper method
- Improve comments to clarify what class-based tasks refers to
- Add documentation about GeneratedActivityContext methods being retained but unused in DF scenarios
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 41 ++++++++++++++------
1 file changed, 30 insertions(+), 11 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index c8185a6e..e221a208 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -230,6 +230,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return null;
}
+ ///
+ /// Determines if code generation should be skipped for Durable Functions scenarios.
+ /// Returns true if only entities exist, since entities don't generate extension methods
+ /// and Durable Functions handles their registration natively.
+ ///
+ static bool ShouldSkipGenerationForDurableFunctions(
+ bool isDurableFunctions,
+ List orchestrators,
+ List activities,
+ ImmutableArray allEvents,
+ ImmutableArray allFunctions)
+ {
+ return isDurableFunctions &&
+ orchestrators.Count == 0 &&
+ activities.Count == 0 &&
+ allEvents.Length == 0 &&
+ allFunctions.Length == 0;
+ }
+
static void Execute(
SourceProductionContext context,
Compilation compilation,
@@ -277,14 +296,10 @@ static void Execute(
// With Durable Functions' native support for class-based invocations (PR #3229),
// we no longer generate [Function] definitions for class-based tasks.
// If we have ONLY entities (no orchestrators, no activities, no events, no method-based functions),
- // then there's nothing to generate for Durable Functions scenarios.
- if (isDurableFunctions &&
- orchestrators.Count == 0 &&
- activities.Count == 0 &&
- allEvents.Length == 0 &&
- allFunctions.Length == 0)
+ // then there's nothing to generate for Durable Functions scenarios since entities don't have
+ // extension methods.
+ if (ShouldSkipGenerationForDurableFunctions(isDurableFunctions, orchestrators, activities, allEvents, allFunctions))
{
- // Only entities remain, and entities don't generate extension methods
return;
}
@@ -312,8 +327,10 @@ public static class GeneratedDurableTaskExtensions
{");
// Note: With Durable Functions' native support for class-based invocations (PR #3229),
- // we no longer generate [Function] definitions for class-based tasks to avoid duplicates.
- // The Durable Functions runtime now handles this automatically.
+ // 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 [DurableTask] attribute). The Durable Functions
+ // runtime now handles function registration for these types automatically.
// We continue to generate extension methods for type-safe invocation.
foreach (DurableTaskTypeInfo orchestrator in orchestrators)
@@ -344,8 +361,10 @@ public static class GeneratedDurableTaskExtensions
AddEventSendMethod(sourceBuilder, eventInfo);
}
- // Note: GeneratedActivityContext is no longer needed since Durable Functions
- // now natively handles class-based invocations without source generation.
+ // Note: The GeneratedActivityContext class and AddGeneratedActivityContextClass method
+ // are no longer needed for Durable Functions since the runtime now natively handles
+ // class-based invocations. These helper methods remain in the codebase but are not
+ // called in Durable Functions scenarios.
if (!isDurableFunctions)
{
From a3568bfc316d787031350d0ffa1461eb610737e4 Mon Sep 17 00:00:00 2001
From: wangbill
Date: Fri, 2 Jan 2026 13:20:40 -0800
Subject: [PATCH 4/9] Update src/Generators/DurableTaskSourceGenerator.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index ede357cd..bb44a141 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -326,12 +326,14 @@ namespace Microsoft.DurableTask
public static class GeneratedDurableTaskExtensions
{");
- // Note: With Durable Functions' native support for class-based invocations (PR #3229),
- // 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 [DurableTask] attribute). The Durable Functions
- // runtime now handles function registration for these types automatically.
- // We continue to generate extension methods for type-safe invocation.
+ // Note: When targeting Azure Functions (Durable Functions scenarios) with native support
+ // for class-based invocations (PR #3229), 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 now handles function registration
+ // for these types automatically in those scenarios. For non-Durable Functions scenarios
+ // (for example, ASP.NET Core using the Durable Task Scheduler), this behavior remains
+ // unchanged. We continue to generate extension methods for type-safe invocation.
foreach (DurableTaskTypeInfo orchestrator in orchestrators)
{
From 288bfd77f2e0f817f813256081d1e5a20edc9287 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 2 Jan 2026 21:32:13 +0000
Subject: [PATCH 5/9] Add version detection to prevent breaking changes for
older Durable Functions versions
- Check if Microsoft.Azure.Functions.Worker.Extensions.DurableTask is version 1.1.0 or later
- Only skip [Function] generation for versions that support native class-based invocation
- For older versions, continue generating [Function] definitions for backward compatibility
- Add TODO to confirm correct version threshold with Durable Functions team
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 103 +++++++++++++++----
1 file changed, 81 insertions(+), 22 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index bb44a141..c7c2a25d 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -232,17 +232,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
///
/// Determines if code generation should be skipped for Durable Functions scenarios.
- /// Returns true if only entities exist, since entities don't generate extension methods
- /// and Durable Functions handles their registration natively.
+ /// 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.
///
static bool ShouldSkipGenerationForDurableFunctions(
- bool isDurableFunctions,
+ bool supportsNativeClassBasedInvocation,
List orchestrators,
List activities,
ImmutableArray allEvents,
ImmutableArray allFunctions)
{
- return isDurableFunctions &&
+ return supportsNativeClassBasedInvocation &&
orchestrators.Count == 0 &&
activities.Count == 0 &&
allEvents.Length == 0 &&
@@ -266,6 +266,24 @@ static void Execute(
bool isDurableFunctions = compilation.ReferencedAssemblyNames.Any(
assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
+ // 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),
+ // we use version 1.1.0 as a conservative threshold. This should be adjusted based on the actual
+ // version where native class-based invocation support was added to the isolated worker extension.
+ // TODO: Confirm the correct version threshold with the Durable Functions team.
+ 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, 1, 0))
+ {
+ supportsNativeClassBasedInvocation = true;
+ }
+ }
+
// Separate tasks into orchestrators, activities, and entities
List orchestrators = new();
List activities = new();
@@ -293,12 +311,12 @@ static void Execute(
return;
}
- // With Durable Functions' native support for class-based invocations (PR #3229),
- // we no longer generate [Function] definitions for class-based tasks.
- // If we have ONLY entities (no orchestrators, no activities, no events, no method-based functions),
- // then there's nothing to generate for Durable Functions scenarios since entities don't have
- // extension methods.
- if (ShouldSkipGenerationForDurableFunctions(isDurableFunctions, orchestrators, activities, allEvents, allFunctions))
+ // 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;
}
@@ -326,17 +344,36 @@ namespace Microsoft.DurableTask
public static class GeneratedDurableTaskExtensions
{");
+ // Generate singleton orchestrator instances for older Durable Functions versions
+ // that don't have native class-based invocation support
+ if (isDurableFunctions && !supportsNativeClassBasedInvocation)
+ {
+ 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), we no longer generate [Function] attribute
+ // 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 now handles function registration
- // for these types automatically in those scenarios. For non-Durable Functions scenarios
- // (for example, ASP.NET Core using the Durable Task Scheduler), this behavior remains
- // unchanged. We continue to generate extension methods for type-safe invocation.
+ // [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)
{
+ // 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)
+ {
+ AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator);
+ }
+
AddOrchestratorCallMethod(sourceBuilder, orchestrator);
AddSubOrchestratorCallMethod(sourceBuilder, orchestrator);
}
@@ -344,9 +381,24 @@ public static class GeneratedDurableTaskExtensions
foreach (DurableTaskTypeInfo activity in activities)
{
AddActivityCallMethod(sourceBuilder, activity);
+
+ // 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)
+ {
+ AddActivityFunctionDeclaration(sourceBuilder, activity);
+ }
}
- // Entities don't have extension methods, so no generation needed for them
+ foreach (DurableTaskTypeInfo entity in entities)
+ {
+ // 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)
+ {
+ AddEntityFunctionDeclaration(sourceBuilder, entity);
+ }
+ }
// Activity function triggers are supported for code-gen (but not orchestration triggers)
IEnumerable activityTriggers = allFunctions.Where(
@@ -363,12 +415,19 @@ public static class GeneratedDurableTaskExtensions
AddEventSendMethod(sourceBuilder, eventInfo);
}
- // Note: The GeneratedActivityContext class and AddGeneratedActivityContextClass method
- // are no longer needed for Durable Functions since the runtime now natively handles
- // class-based invocations. These helper methods remain in the codebase but are not
- // called in Durable Functions scenarios.
-
- 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 with older Durable Functions versions.
+ AddGeneratedActivityContextClass(sourceBuilder);
+ }
+ }
+ else if (!isDurableFunctions)
{
// ASP.NET Core-specific service registration methods
// Only generate if there are actually tasks to register
From 341202f291a05fd83147170dfe14b7f293103f08 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 2 Jan 2026 22:02:57 +0000
Subject: [PATCH 6/9] Update version threshold to 1.11.0 for native class-based
invocation support
- Changed version threshold from 1.1.0 to 1.11.0 based on confirmation from Durable Functions team
- Version 1.11.0 of Microsoft.Azure.Functions.Worker.Extensions.DurableTask added native class-based invocation support
- Removed TODO comment as version has been confirmed
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index c7c2a25d..9100cda1 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -269,16 +269,14 @@ static void Execute(
// 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),
- // we use version 1.1.0 as a conservative threshold. This should be adjusted based on the actual
- // version where native class-based invocation support was added to the isolated worker extension.
- // TODO: Confirm the correct version threshold with the Durable Functions team.
+ // 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, 1, 0))
+ if (durableFunctionsAssembly != null && durableFunctionsAssembly.Version >= new Version(1, 11, 0))
{
supportsNativeClassBasedInvocation = true;
}
From 0e256eb092e3a245345d6ecfaa9398e668658491 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 2 Jan 2026 15:07:10 -0800
Subject: [PATCH 7/9] update samples and tests
---
.../AzureFunctionsApp.csproj | 3 ---
samples/AzureFunctionsApp/Program.cs | 20 +++++++++++++++++++
.../AzureFunctionsApp.Tests.csproj | 1 -
.../AzureFunctionsSmokeTests.csproj | 3 ---
4 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
index 25d824fa..727dd140 100644
--- a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
+++ b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
@@ -5,9 +5,6 @@
v4
Exe
enable
-
- false
- false
diff --git a/samples/AzureFunctionsApp/Program.cs b/samples/AzureFunctionsApp/Program.cs
index 3ec7a407..84357c1b 100644
--- a/samples/AzureFunctionsApp/Program.cs
+++ b/samples/AzureFunctionsApp/Program.cs
@@ -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
@@ -10,6 +16,20 @@ public static void Main()
{
IHost host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
+ .ConfigureServices(services =>
+ {
+ services.Configure(registry =>
+ {
+ registry
+ .AddOrchestrator()
+ .AddOrchestrator()
+ .AddActivity()
+ .AddActivity()
+ .AddEntity()
+ .AddEntity()
+ .AddEntity();
+ });
+ })
.Build();
host.Run();
diff --git a/samples/AzureFunctionsUnitTests/AzureFunctionsApp.Tests.csproj b/samples/AzureFunctionsUnitTests/AzureFunctionsApp.Tests.csproj
index e1e548ad..585e053a 100644
--- a/samples/AzureFunctionsUnitTests/AzureFunctionsApp.Tests.csproj
+++ b/samples/AzureFunctionsUnitTests/AzureFunctionsApp.Tests.csproj
@@ -4,7 +4,6 @@
net6.0;net8.0
enable
enable
-
false
true
diff --git a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj
index 219d5bc9..28263eec 100644
--- a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj
+++ b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj
@@ -8,9 +8,6 @@
false
false
-
- false
- false
From 1cd06bb4f2ade6853f6cbb3f26419c2e34e1623c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:34:07 +0000
Subject: [PATCH 8/9] Fix smoke test CI failure by adding class-based task
registration
- Add explicit registration of orchestrators, activities, and entities in smoke test Program.cs
- Register GeneratedOrchestration, ChildGeneratedOrchestration, CountCharactersActivity, and GeneratorCounter
- Required for Durable Functions v1.11.0+ which has native class-based invocation support
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
test/AzureFunctionsSmokeTests/Program.cs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/test/AzureFunctionsSmokeTests/Program.cs b/test/AzureFunctionsSmokeTests/Program.cs
index eddb3547..bd62e50b 100644
--- a/test/AzureFunctionsSmokeTests/Program.cs
+++ b/test/AzureFunctionsSmokeTests/Program.cs
@@ -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;
@@ -11,6 +13,17 @@ public static void Main()
{
IHost host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
+ .ConfigureServices(services =>
+ {
+ services.Configure(registry =>
+ {
+ registry
+ .AddOrchestrator()
+ .AddOrchestrator()
+ .AddActivity()
+ .AddEntity();
+ });
+ })
.Build();
host.Run();
From db10383e392a6a0d6caf48dfa61b751ba5cf7f4e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 02:04:11 +0000
Subject: [PATCH 9/9] Fix ProjectTypeConfigurationTests for Durable Functions
v1.11.0+ behavior
- Update tests to expect only extension methods (no [Function] definitions) for class-based tasks
- Tests were failing because they expected old behavior before v1.11.0
- With v1.11.0+, Durable Functions runtime handles class-based tasks natively
- Updated 5 test methods to align with the new generator behavior
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
.../ProjectTypeConfigurationTests.cs | 109 ++----------------
1 file changed, 11 insertions(+), 98 deletions(-)
diff --git a/test/Generators.Tests/ProjectTypeConfigurationTests.cs b/test/Generators.Tests/ProjectTypeConfigurationTests.cs
index bcac0a3d..f559baf8 100644
--- a/test/Generators.Tests/ProjectTypeConfigurationTests.cs
+++ b/test/Generators.Tests/ProjectTypeConfigurationTests.cs
@@ -110,6 +110,8 @@ public Task ExplicitFunctionsMode_WithoutFunctionsReference_GeneratesFunctionsCo
{
// Test that explicit "Functions" configuration generates Functions code
// even without Functions references
+ // Note: With Durable Functions v1.11.0+, only extension methods are generated,
+ // not [Function] definitions, as the runtime handles class-based tasks natively
string code = @"
using System.Threading.Tasks;
using Microsoft.DurableTask;
@@ -120,7 +122,7 @@ class MyActivity : TaskActivity
public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
}";
- // With explicit "Functions", we should get Functions code (Activity trigger function)
+ // With explicit "Functions" and version >= 1.11.0, we only get extension methods
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: @"
@@ -131,28 +133,6 @@ class MyActivity : TaskActivity
public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
{
return ctx.CallActivityAsync(""MyActivity"", input, options);
-}
-
-[Function(nameof(MyActivity))]
-public static async Task MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
-{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
- TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
- object? result = await activity.RunAsync(context, input);
- return (string)result!;
-}
-
-sealed class GeneratedActivityContext : TaskActivityContext
-{
- public GeneratedActivityContext(TaskName name, string instanceId)
- {
- this.Name = name;
- this.InstanceId = instanceId;
- }
-
- public override TaskName Name { get; }
-
- public override string InstanceId { get; }
}",
isDurableFunctions: true);
@@ -170,6 +150,8 @@ public GeneratedActivityContext(TaskName name, string instanceId)
public Task ExplicitFunctionsMode_OrchestratorTest()
{
// Test that "Functions" mode generates orchestrator Functions code
+ // Note: With Durable Functions v1.11.0+, only extension methods are generated,
+ // not [Function] definitions, as the runtime handles class-based tasks natively
string code = @"
using System.Threading.Tasks;
using Microsoft.DurableTask;
@@ -183,15 +165,6 @@ class MyOrchestrator : TaskOrchestrator
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: @"
-static readonly ITaskOrchestrator singletonMyOrchestrator = new MyOrchestrator();
-
-[Function(nameof(MyOrchestrator))]
-public static Task MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
-{
- return singletonMyOrchestrator.RunAsync(context, context.GetInput())
- .ContinueWith(t => (string)(t.Result ?? default(string)!), TaskContinuationOptions.ExecuteSynchronously);
-}
-
///
/// Schedules a new instance of the orchestrator.
///
@@ -225,6 +198,8 @@ public static Task CallMyOrchestratorAsync(
public Task AutoMode_WithFunctionsReference_GeneratesFunctionsCode()
{
// Test that "Auto" mode falls back to auto-detection
+ // Note: With Durable Functions v1.11.0+, only extension methods are generated,
+ // not [Function] definitions, as the runtime handles class-based tasks natively
string code = @"
using System.Threading.Tasks;
using Microsoft.DurableTask;
@@ -245,28 +220,6 @@ class MyActivity : TaskActivity
public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
{
return ctx.CallActivityAsync(""MyActivity"", input, options);
-}
-
-[Function(nameof(MyActivity))]
-public static async Task MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
-{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
- TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
- object? result = await activity.RunAsync(context, input);
- return (string)result!;
-}
-
-sealed class GeneratedActivityContext : TaskActivityContext
-{
- public GeneratedActivityContext(TaskName name, string instanceId)
- {
- this.Name = name;
- this.InstanceId = instanceId;
- }
-
- public override TaskName Name { get; }
-
- public override string InstanceId { get; }
}",
isDurableFunctions: true);
@@ -323,6 +276,8 @@ internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistr
public Task UnrecognizedMode_WithFunctionsReference_FallsBackToAutoDetection()
{
// Test that unrecognized values fall back to auto-detection
+ // Note: With Durable Functions v1.11.0+, only extension methods are generated,
+ // not [Function] definitions, as the runtime handles class-based tasks natively
string code = @"
using System.Threading.Tasks;
using Microsoft.DurableTask;
@@ -343,28 +298,6 @@ class MyActivity : TaskActivity
public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
{
return ctx.CallActivityAsync(""MyActivity"", input, options);
-}
-
-[Function(nameof(MyActivity))]
-public static async Task MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
-{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
- TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
- object? result = await activity.RunAsync(context, input);
- return (string)result!;
-}
-
-sealed class GeneratedActivityContext : TaskActivityContext
-{
- public GeneratedActivityContext(TaskName name, string instanceId)
- {
- this.Name = name;
- this.InstanceId = instanceId;
- }
-
- public override TaskName Name { get; }
-
- public override string InstanceId { get; }
}",
isDurableFunctions: true);
@@ -421,6 +354,8 @@ internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistr
public Task NullProjectType_WithFunctionsReference_GeneratesFunctionsCode()
{
// Test that null projectType (default) with Functions reference falls back to auto-detection
+ // Note: With Durable Functions v1.11.0+, only extension methods are generated,
+ // not [Function] definitions, as the runtime handles class-based tasks natively
string code = @"
using System.Threading.Tasks;
using Microsoft.DurableTask;
@@ -441,28 +376,6 @@ class MyActivity : TaskActivity
public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
{
return ctx.CallActivityAsync(""MyActivity"", input, options);
-}
-
-[Function(nameof(MyActivity))]
-public static async Task MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
-{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
- TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
- object? result = await activity.RunAsync(context, input);
- return (string)result!;
-}
-
-sealed class GeneratedActivityContext : TaskActivityContext
-{
- public GeneratedActivityContext(TaskName name, string instanceId)
- {
- this.Name = name;
- this.InstanceId = instanceId;
- }
-
- public override TaskName Name { get; }
-
- public override string InstanceId { get; }
}",
isDurableFunctions: true);