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/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs index 0116e568..6c515e26 100644 --- a/src/Generators/DurableTaskSourceGenerator.cs +++ b/src/Generators/DurableTaskSourceGenerator.cs @@ -272,6 +272,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } /// + /// Determines if code generation should be skipped for Durable Functions scenarios. + /// Returns true if only entities exist and the runtime supports native class-based invocation, + /// since entities don't generate extension methods and the runtime handles their registration. + /// + static bool ShouldSkipGenerationForDurableFunctions( + bool supportsNativeClassBasedInvocation, + List orchestrators, + List activities, + ImmutableArray allEvents, + ImmutableArray allFunctions) + { + return supportsNativeClassBasedInvocation && + orchestrators.Count == 0 && + activities.Count == 0 && + allEvents.Length == 0 && + allFunctions.Length == 0; + } + /// Checks if a name is a valid C# identifier. /// /// The name to validate. @@ -322,6 +340,22 @@ static void Execute( // Determine if we should generate Durable Functions specific code bool isDurableFunctions = DetermineIsDurableFunctions(compilation, allFunctions, projectType); + // Check if the Durable Functions extension version supports native class-based invocation. + // This feature was introduced in PR #3229: https://github.com/Azure/azure-functions-durable-extension/pull/3229 + // For the isolated worker extension (Microsoft.Azure.Functions.Worker.Extensions.DurableTask), + // native class-based invocation support was added in version 1.11.0. + bool supportsNativeClassBasedInvocation = false; + if (isDurableFunctions) + { + var durableFunctionsAssembly = compilation.ReferencedAssemblyNames.FirstOrDefault( + assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase)); + + if (durableFunctionsAssembly != null && durableFunctionsAssembly.Version >= new Version(1, 11, 0)) + { + supportsNativeClassBasedInvocation = true; + } + } + // Separate tasks into orchestrators, activities, and entities // Skip tasks with invalid names to avoid generating invalid code List orchestrators = new(); @@ -358,6 +392,16 @@ static void Execute( return; } + // With Durable Functions' native support for class-based invocations (PR #3229, v3.8.0+), + // we no longer generate [Function] definitions for class-based tasks when the runtime + // supports native invocation. If we have ONLY entities (no orchestrators, no activities, + // no events, no method-based functions), then there's nothing to generate for those + // scenarios since entities don't have extension methods. + if (ShouldSkipGenerationForDurableFunctions(supportsNativeClassBasedInvocation, orchestrators, activities, allEvents, allFunctions)) + { + return; + } + StringBuilder sourceBuilder = new(capacity: found * 1024); sourceBuilder.Append(@"// #nullable enable @@ -380,9 +424,11 @@ namespace Microsoft.DurableTask { public static class GeneratedDurableTaskExtensions {"); - if (isDurableFunctions) + + // Generate singleton orchestrator instances for older Durable Functions versions + // that don't have native class-based invocation support + if (isDurableFunctions && !supportsNativeClassBasedInvocation) { - // Generate a singleton orchestrator object instance that can be reused for all invocations. foreach (DurableTaskTypeInfo orchestrator in orchestrators) { sourceBuilder.AppendLine($@" @@ -390,11 +436,22 @@ public static class GeneratedDurableTaskExtensions } } + // Note: When targeting Azure Functions (Durable Functions scenarios) with native support + // for class-based invocations (PR #3229, v3.8.0+), we no longer generate [Function] attribute + // definitions for class-based orchestrators, activities, and entities (i.e., classes that + // implement ITaskOrchestrator, ITaskActivity, or ITaskEntity and are decorated with the + // [DurableTask] attribute). The Durable Functions runtime handles function registration + // for these types automatically in those scenarios. For older versions of Durable Functions + // (prior to v3.8.0) or non-Durable Functions scenarios (for example, ASP.NET Core using + // the Durable Task Scheduler), we continue to generate [Function] definitions. + // We always generate extension methods for type-safe invocation. + foreach (DurableTaskTypeInfo orchestrator in orchestrators) { - if (isDurableFunctions) + // Only generate [Function] definitions for Durable Functions if the runtime doesn't + // support native class-based invocation (versions prior to v3.8.0) + if (isDurableFunctions && !supportsNativeClassBasedInvocation) { - // Generate the function definition required to trigger orchestrators in Azure Functions AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator); } @@ -406,18 +463,20 @@ public static class GeneratedDurableTaskExtensions { AddActivityCallMethod(sourceBuilder, activity); - if (isDurableFunctions) + // Only generate [Function] definitions for Durable Functions if the runtime doesn't + // support native class-based invocation (versions prior to v3.8.0) + if (isDurableFunctions && !supportsNativeClassBasedInvocation) { - // Generate the function definition required to trigger activities in Azure Functions AddActivityFunctionDeclaration(sourceBuilder, activity); } } foreach (DurableTaskTypeInfo entity in entities) { - if (isDurableFunctions) + // Only generate [Function] definitions for Durable Functions if the runtime doesn't + // support native class-based invocation (versions prior to v3.8.0) + if (isDurableFunctions && !supportsNativeClassBasedInvocation) { - // Generate the function definition required to trigger entities in Azure Functions AddEntityFunctionDeclaration(sourceBuilder, entity); } } @@ -437,16 +496,19 @@ public static class GeneratedDurableTaskExtensions AddEventSendMethod(sourceBuilder, eventInfo); } - if (isDurableFunctions) + // Note: The GeneratedActivityContext class is only needed for older versions of + // Durable Functions (prior to v3.8.0) that don't have native class-based invocation support. + // For v3.8.0+, the runtime handles class-based invocations natively. + if (isDurableFunctions && !supportsNativeClassBasedInvocation) { if (activities.Count > 0) { // Functions-specific helper class, which is only needed when - // using the class-based syntax. + // using the class-based syntax with older Durable Functions versions. AddGeneratedActivityContextClass(sourceBuilder); } } - else + else if (!isDurableFunctions) { // ASP.NET Core-specific service registration methods // Only generate if there are actually tasks to register diff --git a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj index ccb904fc..c248ba8c 100644 --- a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj +++ b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj @@ -10,9 +10,6 @@ false false - - false - false 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(); diff --git a/test/Generators.Tests/AzureFunctionsTests.cs b/test/Generators.Tests/AzureFunctionsTests.cs index 3a02eeee..8ace515c 100644 --- a/test/Generators.Tests/AzureFunctionsTests.cs +++ b/test/Generators.Tests/AzureFunctionsTests.cs @@ -192,7 +192,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. @@ -216,13 +217,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: $@" @@ -233,17 +227,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( @@ -256,7 +240,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. @@ -294,15 +279,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. /// @@ -334,7 +310,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. @@ -377,15 +354,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. /// @@ -415,8 +383,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] @@ -439,26 +408,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] @@ -486,26 +446,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() @@ -530,26 +481,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() @@ -585,15 +529,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. /// @@ -621,23 +556,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/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); diff --git a/test/Generators.Tests/Utils/TestHelpers.cs b/test/Generators.Tests/Utils/TestHelpers.cs index 92f3db86..f43ab2c1 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() { return RunTestAsync( @@ -38,10 +38,6 @@ public static Task RunTestAsync( TestState = { Sources = { inputSource }, - GeneratedSources = - { - (typeof(TSourceGenerator), expectedFileName, SourceText.From(expectedOutputSource, Encoding.UTF8, SourceHashAlgorithm.Sha256)), - }, AdditionalReferences = { // Durable Task SDK @@ -50,6 +46,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