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);