diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs index 0b4e717e..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, @@ -274,6 +293,16 @@ 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)) + { + return; + } + StringBuilder sourceBuilder = new(capacity: found * 1024); sourceBuilder.Append(@"// #nullable enable @@ -296,24 +325,16 @@ 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] 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) { - if (isDurableFunctions) - { - // Generate the function definition required to trigger orchestrators in Azure Functions - AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator); - } - AddOrchestratorCallMethod(sourceBuilder, orchestrator); AddSubOrchestratorCallMethod(sourceBuilder, orchestrator); } @@ -321,22 +342,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 +361,12 @@ 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: 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) { // 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