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