Skip to content

Commit d4676ae

Browse files
CopilotYunchuWang
andcommitted
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 <[email protected]>
1 parent 071dfdd commit d4676ae

File tree

3 files changed

+56
-149
lines changed

3 files changed

+56
-149
lines changed

src/Generators/DurableTaskSourceGenerator.cs

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,20 @@ static void Execute(
274274
return;
275275
}
276276

277+
// With Durable Functions' native support for class-based invocations (PR #3229),
278+
// we no longer generate [Function] definitions for class-based tasks.
279+
// If we have ONLY entities (no orchestrators, no activities, no events, no method-based functions),
280+
// then there's nothing to generate for Durable Functions scenarios.
281+
if (isDurableFunctions &&
282+
orchestrators.Count == 0 &&
283+
activities.Count == 0 &&
284+
allEvents.Length == 0 &&
285+
allFunctions.Length == 0)
286+
{
287+
// Only entities remain, and entities don't generate extension methods
288+
return;
289+
}
290+
277291
StringBuilder sourceBuilder = new(capacity: found * 1024);
278292
sourceBuilder.Append(@"// <auto-generated/>
279293
#nullable enable
@@ -296,47 +310,24 @@ namespace Microsoft.DurableTask
296310
{
297311
public static class GeneratedDurableTaskExtensions
298312
{");
299-
if (isDurableFunctions)
300-
{
301-
// Generate a singleton orchestrator object instance that can be reused for all invocations.
302-
foreach (DurableTaskTypeInfo orchestrator in orchestrators)
303-
{
304-
sourceBuilder.AppendLine($@"
305-
static readonly ITaskOrchestrator singleton{orchestrator.TaskName} = new {orchestrator.TypeName}();");
306-
}
307-
}
313+
314+
// Note: With Durable Functions' native support for class-based invocations (PR #3229),
315+
// we no longer generate [Function] definitions for class-based tasks to avoid duplicates.
316+
// The Durable Functions runtime now handles this automatically.
317+
// We continue to generate extension methods for type-safe invocation.
308318

309319
foreach (DurableTaskTypeInfo orchestrator in orchestrators)
310320
{
311-
if (isDurableFunctions)
312-
{
313-
// Generate the function definition required to trigger orchestrators in Azure Functions
314-
AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator);
315-
}
316-
317321
AddOrchestratorCallMethod(sourceBuilder, orchestrator);
318322
AddSubOrchestratorCallMethod(sourceBuilder, orchestrator);
319323
}
320324

321325
foreach (DurableTaskTypeInfo activity in activities)
322326
{
323327
AddActivityCallMethod(sourceBuilder, activity);
324-
325-
if (isDurableFunctions)
326-
{
327-
// Generate the function definition required to trigger activities in Azure Functions
328-
AddActivityFunctionDeclaration(sourceBuilder, activity);
329-
}
330328
}
331329

332-
foreach (DurableTaskTypeInfo entity in entities)
333-
{
334-
if (isDurableFunctions)
335-
{
336-
// Generate the function definition required to trigger entities in Azure Functions
337-
AddEntityFunctionDeclaration(sourceBuilder, entity);
338-
}
339-
}
330+
// Entities don't have extension methods, so no generation needed for them
340331

341332
// Activity function triggers are supported for code-gen (but not orchestration triggers)
342333
IEnumerable<DurableFunction> activityTriggers = allFunctions.Where(
@@ -353,16 +344,10 @@ public static class GeneratedDurableTaskExtensions
353344
AddEventSendMethod(sourceBuilder, eventInfo);
354345
}
355346

356-
if (isDurableFunctions)
357-
{
358-
if (activities.Count > 0)
359-
{
360-
// Functions-specific helper class, which is only needed when
361-
// using the class-based syntax.
362-
AddGeneratedActivityContextClass(sourceBuilder);
363-
}
364-
}
365-
else
347+
// Note: GeneratedActivityContext is no longer needed since Durable Functions
348+
// now natively handles class-based invocations without source generation.
349+
350+
if (!isDurableFunctions)
366351
{
367352
// ASP.NET Core-specific service registration methods
368353
// Only generate if there are actually tasks to register

test/Generators.Tests/AzureFunctionsTests.cs

Lines changed: 24 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
119119

120120
/// <summary>
121121
/// Verifies that using the class-based activity syntax generates a <see cref="TaskOrchestrationContext"/>
122-
/// extension method as well as an <see cref="ActivityTriggerAttribute"/> function definition.
122+
/// extension method. With PR #3229, Durable Functions now natively handles class-based invocations,
123+
/// so the generator no longer creates [Function] attribute definitions to avoid duplicates.
123124
/// </summary>
124125
/// <param name="inputType">The activity input type.</param>
125126
/// <param name="outputType">The activity output type.</param>
@@ -143,13 +144,6 @@ public class MyActivity : TaskActivity<{inputType}, {outputType}>
143144
public override Task<{outputType}> RunAsync(TaskActivityContext context, {inputType} input) => Task.FromResult<{outputType}>(default!);
144145
}}";
145146

146-
// Build the expected InputParameter format (matches generator logic)
147-
string expectedInputParameter = inputType + " input";
148-
if (inputType.EndsWith('?'))
149-
{
150-
expectedInputParameter += " = default";
151-
}
152-
153147
string expectedOutput = TestHelpers.WrapAndFormat(
154148
GeneratedClassName,
155149
methodList: $@"
@@ -160,17 +154,7 @@ public class MyActivity : TaskActivity<{inputType}, {outputType}>
160154
public static Task<{outputType}> CallMyActivityAsync(this TaskOrchestrationContext ctx, {inputType} input, TaskOptions? options = null)
161155
{{
162156
return ctx.CallActivityAsync<{outputType}>(""MyActivity"", input, options);
163-
}}
164-
165-
[Function(nameof(MyActivity))]
166-
public static async Task<{outputType}> MyActivity([ActivityTrigger] {expectedInputParameter}, string instanceId, FunctionContext executionContext)
167-
{{
168-
ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance<MyActivity>(executionContext.InstanceServices);
169-
TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
170-
object? result = await activity.RunAsync(context, input);
171-
return ({outputType})result!;
172-
}}
173-
{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
157+
}}",
174158
isDurableFunctions: true);
175159

176160
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
@@ -183,7 +167,8 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
183167
/// <summary>
184168
/// Verifies that using the class-based syntax for authoring orchestrations generates
185169
/// type-safe <see cref="DurableTaskClient"/> and <see cref="TaskOrchestrationContext"/>
186-
/// extension methods as well as <see cref="OrchestrationTriggerAttribute"/> function triggers.
170+
/// extension methods. With PR #3229, Durable Functions now natively handles class-based
171+
/// invocations, so the generator no longer creates [Function] attribute definitions.
187172
/// </summary>
188173
/// <param name="inputType">The activity input type.</param>
189174
/// <param name="outputType">The activity output type.</param>
@@ -221,15 +206,6 @@ public class MyOrchestrator : TaskOrchestrator<{inputType}, {outputType}>
221206
string expectedOutput = TestHelpers.WrapAndFormat(
222207
GeneratedClassName,
223208
methodList: $@"
224-
static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
225-
226-
[Function(nameof(MyOrchestrator))]
227-
public static Task<{outputType}> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
228-
{{
229-
return singletonMyOrchestrator.RunAsync(context, context.GetInput<{inputType}>())
230-
.ContinueWith(t => ({outputType})(t.Result ?? default({outputType})!), TaskContinuationOptions.ExecuteSynchronously);
231-
}}
232-
233209
/// <summary>
234210
/// Schedules a new instance of the <see cref=""MyNS.MyOrchestrator""/> orchestrator.
235211
/// </summary>
@@ -261,7 +237,8 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
261237
/// <summary>
262238
/// Verifies that using the class-based syntax for authoring orchestrations generates
263239
/// type-safe <see cref="DurableTaskClient"/> and <see cref="TaskOrchestrationContext"/>
264-
/// extension methods as well as <see cref="OrchestrationTriggerAttribute"/> function triggers.
240+
/// extension methods. With PR #3229, Durable Functions now natively handles class-based
241+
/// invocations, so the generator no longer creates [Function] attribute definitions.
265242
/// </summary>
266243
/// <param name="inputType">The activity input type.</param>
267244
/// <param name="outputType">The activity output type.</param>
@@ -304,15 +281,6 @@ public abstract class MyOrchestratorBase : TaskOrchestrator<{inputType}, {output
304281
string expectedOutput = TestHelpers.WrapAndFormat(
305282
GeneratedClassName,
306283
methodList: $@"
307-
static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
308-
309-
[Function(nameof(MyOrchestrator))]
310-
public static Task<{outputType}> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
311-
{{
312-
return singletonMyOrchestrator.RunAsync(context, context.GetInput<{inputType}>())
313-
.ContinueWith(t => ({outputType})(t.Result ?? default({outputType})!), TaskContinuationOptions.ExecuteSynchronously);
314-
}}
315-
316284
/// <summary>
317285
/// Schedules a new instance of the <see cref=""MyNS.MyOrchestrator""/> orchestrator.
318286
/// </summary>
@@ -342,8 +310,9 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
342310
}
343311

344312
/// <summary>
345-
/// Verifies that using the class-based syntax for authoring entities generates
346-
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
313+
/// Verifies that using the class-based syntax for authoring entities no longer generates
314+
/// any code for Azure Functions. With PR #3229, Durable Functions now natively handles
315+
/// class-based invocations. Entities don't have extension methods, so nothing is generated.
347316
/// </summary>
348317
/// <param name="stateType">The entity state type.</param>
349318
[Theory]
@@ -366,26 +335,17 @@ public class MyEntity : TaskEntity<{stateType}>
366335
}}
367336
}}";
368337

369-
string expectedOutput = TestHelpers.WrapAndFormat(
370-
GeneratedClassName,
371-
methodList: @"
372-
[Function(nameof(MyEntity))]
373-
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
374-
{
375-
return dispatcher.DispatchAsync<MyNS.MyEntity>();
376-
}",
377-
isDurableFunctions: true);
378-
338+
// With PR #3229, no code is generated for class-based entities in Durable Functions
379339
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
380340
GeneratedFileName,
381341
code,
382-
expectedOutput,
342+
expectedOutputSource: null, // No output expected
383343
isDurableFunctions: true);
384344
}
385345

386346
/// <summary>
387-
/// Verifies that using the class-based syntax for authoring entities with inheritance generates
388-
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
347+
/// Verifies that using the class-based syntax for authoring entities with inheritance no longer generates
348+
/// any code for Azure Functions. With PR #3229, Durable Functions now natively handles class-based invocations.
389349
/// </summary>
390350
/// <param name="stateType">The entity state type.</param>
391351
[Theory]
@@ -413,26 +373,17 @@ public abstract class MyEntityBase : TaskEntity<{stateType}>
413373
}}
414374
}}";
415375

416-
string expectedOutput = TestHelpers.WrapAndFormat(
417-
GeneratedClassName,
418-
methodList: @"
419-
[Function(nameof(MyEntity))]
420-
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
421-
{
422-
return dispatcher.DispatchAsync<MyNS.MyEntity>();
423-
}",
424-
isDurableFunctions: true);
425-
376+
// With PR #3229, no code is generated for class-based entities in Durable Functions
426377
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
427378
GeneratedFileName,
428379
code,
429-
expectedOutput,
380+
expectedOutputSource: null, // No output expected
430381
isDurableFunctions: true);
431382
}
432383

433384
/// <summary>
434-
/// Verifies that using the class-based syntax for authoring entities with custom state types generates
435-
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
385+
/// Verifies that using the class-based syntax for authoring entities with custom state types no longer generates
386+
/// any code for Azure Functions. With PR #3229, Durable Functions now natively handles class-based invocations.
436387
/// </summary>
437388
[Fact]
438389
public async Task Entities_ClassBasedSyntax_CustomStateType()
@@ -457,26 +408,19 @@ public class MyEntity : TaskEntity<MyState>
457408
}
458409
}";
459410

460-
string expectedOutput = TestHelpers.WrapAndFormat(
461-
GeneratedClassName,
462-
methodList: @"
463-
[Function(nameof(MyEntity))]
464-
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
465-
{
466-
return dispatcher.DispatchAsync<MyNS.MyEntity>();
467-
}",
468-
isDurableFunctions: true);
469-
411+
// With PR #3229, no code is generated for class-based entities in Durable Functions
470412
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
471413
GeneratedFileName,
472414
code,
473-
expectedOutput,
415+
expectedOutputSource: null, // No output expected
474416
isDurableFunctions: true);
475417
}
476418

477419
/// <summary>
478420
/// Verifies that using the class-based syntax for authoring a mix of orchestrators, activities,
479-
/// and entities generates the appropriate function triggers for Azure Functions.
421+
/// and entities generates the appropriate extension methods for Azure Functions.
422+
/// With PR #3229, Durable Functions now natively handles class-based invocations,
423+
/// so the generator no longer creates [Function] attribute definitions.
480424
/// </summary>
481425
[Fact]
482426
public async Task Mixed_OrchestratorActivityEntity_ClassBasedSyntax()
@@ -512,15 +456,6 @@ public class MyEntity : TaskEntity<int>
512456
string expectedOutput = TestHelpers.WrapAndFormat(
513457
GeneratedClassName,
514458
methodList: $@"
515-
static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
516-
517-
[Function(nameof(MyOrchestrator))]
518-
public static Task<string> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
519-
{{
520-
return singletonMyOrchestrator.RunAsync(context, context.GetInput<int>())
521-
.ContinueWith(t => (string)(t.Result ?? default(string)!), TaskContinuationOptions.ExecuteSynchronously);
522-
}}
523-
524459
/// <summary>
525460
/// Schedules a new instance of the <see cref=""MyNS.MyOrchestrator""/> orchestrator.
526461
/// </summary>
@@ -548,23 +483,7 @@ public static Task<string> CallMyOrchestratorAsync(
548483
public static Task<string> CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
549484
{{
550485
return ctx.CallActivityAsync<string>(""MyActivity"", input, options);
551-
}}
552-
553-
[Function(nameof(MyActivity))]
554-
public static async Task<string> MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
555-
{{
556-
ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance<MyNS.MyActivity>(executionContext.InstanceServices);
557-
TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
558-
object? result = await activity.RunAsync(context, input);
559-
return (string)result!;
560-
}}
561-
562-
[Function(nameof(MyEntity))]
563-
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
564-
{{
565-
return dispatcher.DispatchAsync<MyNS.MyEntity>();
566-
}}
567-
{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
486+
}}",
568487
isDurableFunctions: true);
569488

570489
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(

test/Generators.Tests/Utils/TestHelpers.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,14 @@ static class TestHelpers
1515
public static Task RunTestAsync<TSourceGenerator>(
1616
string expectedFileName,
1717
string inputSource,
18-
string expectedOutputSource,
18+
string? expectedOutputSource,
1919
bool isDurableFunctions) where TSourceGenerator : IIncrementalGenerator, new()
2020
{
2121
CSharpSourceGeneratorVerifier<TSourceGenerator>.Test test = new()
2222
{
2323
TestState =
2424
{
2525
Sources = { inputSource },
26-
GeneratedSources =
27-
{
28-
(typeof(TSourceGenerator), expectedFileName, SourceText.From(expectedOutputSource, Encoding.UTF8, SourceHashAlgorithm.Sha256)),
29-
},
3026
AdditionalReferences =
3127
{
3228
// Durable Task SDK
@@ -35,6 +31,13 @@ public static Task RunTestAsync<TSourceGenerator>(
3531
},
3632
};
3733

34+
// Only add generated source if expectedOutputSource is not null
35+
if (expectedOutputSource != null)
36+
{
37+
test.TestState.GeneratedSources.Add(
38+
(typeof(TSourceGenerator), expectedFileName, SourceText.From(expectedOutputSource, Encoding.UTF8, SourceHashAlgorithm.Sha256)));
39+
}
40+
3841
if (isDurableFunctions)
3942
{
4043
// Durable Functions code generation is triggered by the presence of the

0 commit comments

Comments
 (0)