Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 43 additions & 39 deletions src/Generators/DurableTaskSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return null;
}

/// <summary>
/// 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.
/// </summary>
static bool ShouldSkipGenerationForDurableFunctions(
bool isDurableFunctions,
List<DurableTaskTypeInfo> orchestrators,
List<DurableTaskTypeInfo> activities,
ImmutableArray<DurableEventTypeInfo> allEvents,
ImmutableArray<DurableFunction> allFunctions)
{
return isDurableFunctions &&
orchestrators.Count == 0 &&
activities.Count == 0 &&
allEvents.Length == 0 &&
allFunctions.Length == 0;
}

static void Execute(
SourceProductionContext context,
Compilation compilation,
Expand Down Expand Up @@ -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(@"// <auto-generated/>
#nullable enable
Expand All @@ -296,47 +325,26 @@ 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);
}

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<DurableFunction> activityTriggers = allFunctions.Where(
Expand All @@ -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
Expand Down
129 changes: 24 additions & 105 deletions test/Generators.Tests/AzureFunctionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(

/// <summary>
/// Verifies that using the class-based activity syntax generates a <see cref="TaskOrchestrationContext"/>
/// extension method as well as an <see cref="ActivityTriggerAttribute"/> 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.
/// </summary>
/// <param name="inputType">The activity input type.</param>
/// <param name="outputType">The activity output type.</param>
Expand All @@ -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: $@"
Expand All @@ -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<MyActivity>(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<DurableTaskSourceGenerator>(
Expand All @@ -183,7 +167,8 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
/// <summary>
/// Verifies that using the class-based syntax for authoring orchestrations generates
/// type-safe <see cref="DurableTaskClient"/> and <see cref="TaskOrchestrationContext"/>
/// extension methods as well as <see cref="OrchestrationTriggerAttribute"/> function triggers.
/// extension methods. With PR #3229, Durable Functions now natively handles class-based
/// invocations, so the generator no longer creates [Function] attribute definitions.
/// </summary>
/// <param name="inputType">The activity input type.</param>
/// <param name="outputType">The activity output type.</param>
Expand Down Expand Up @@ -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);
}}

/// <summary>
/// Schedules a new instance of the <see cref=""MyNS.MyOrchestrator""/> orchestrator.
/// </summary>
Expand Down Expand Up @@ -261,7 +237,8 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
/// <summary>
/// Verifies that using the class-based syntax for authoring orchestrations generates
/// type-safe <see cref="DurableTaskClient"/> and <see cref="TaskOrchestrationContext"/>
/// extension methods as well as <see cref="OrchestrationTriggerAttribute"/> function triggers.
/// extension methods. With PR #3229, Durable Functions now natively handles class-based
/// invocations, so the generator no longer creates [Function] attribute definitions.
/// </summary>
/// <param name="inputType">The activity input type.</param>
/// <param name="outputType">The activity output type.</param>
Expand Down Expand Up @@ -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);
}}

/// <summary>
/// Schedules a new instance of the <see cref=""MyNS.MyOrchestrator""/> orchestrator.
/// </summary>
Expand Down Expand Up @@ -342,8 +310,9 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
}

/// <summary>
/// Verifies that using the class-based syntax for authoring entities generates
/// <see cref="EntityTriggerAttribute"/> 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.
/// </summary>
/// <param name="stateType">The entity state type.</param>
[Theory]
Expand All @@ -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<MyNS.MyEntity>();
}",
isDurableFunctions: true);

// With PR #3229, no code is generated for class-based entities in Durable Functions
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
GeneratedFileName,
code,
expectedOutput,
expectedOutputSource: null, // No output expected
isDurableFunctions: true);
}

/// <summary>
/// Verifies that using the class-based syntax for authoring entities with inheritance generates
/// <see cref="EntityTriggerAttribute"/> 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.
/// </summary>
/// <param name="stateType">The entity state type.</param>
[Theory]
Expand Down Expand Up @@ -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<MyNS.MyEntity>();
}",
isDurableFunctions: true);

// With PR #3229, no code is generated for class-based entities in Durable Functions
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
GeneratedFileName,
code,
expectedOutput,
expectedOutputSource: null, // No output expected
isDurableFunctions: true);
}

/// <summary>
/// Verifies that using the class-based syntax for authoring entities with custom state types generates
/// <see cref="EntityTriggerAttribute"/> 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.
/// </summary>
[Fact]
public async Task Entities_ClassBasedSyntax_CustomStateType()
Expand All @@ -457,26 +408,19 @@ public class MyEntity : TaskEntity<MyState>
}
}";

string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: @"
[Function(nameof(MyEntity))]
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
{
return dispatcher.DispatchAsync<MyNS.MyEntity>();
}",
isDurableFunctions: true);

// With PR #3229, no code is generated for class-based entities in Durable Functions
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
GeneratedFileName,
code,
expectedOutput,
expectedOutputSource: null, // No output expected
isDurableFunctions: true);
}

/// <summary>
/// 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.
/// </summary>
[Fact]
public async Task Mixed_OrchestratorActivityEntity_ClassBasedSyntax()
Expand Down Expand Up @@ -512,15 +456,6 @@ public class MyEntity : TaskEntity<int>
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: $@"
static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();

[Function(nameof(MyOrchestrator))]
public static Task<string> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
{{
return singletonMyOrchestrator.RunAsync(context, context.GetInput<int>())
.ContinueWith(t => (string)(t.Result ?? default(string)!), TaskContinuationOptions.ExecuteSynchronously);
}}

/// <summary>
/// Schedules a new instance of the <see cref=""MyNS.MyOrchestrator""/> orchestrator.
/// </summary>
Expand Down Expand Up @@ -548,23 +483,7 @@ public static Task<string> CallMyOrchestratorAsync(
public static Task<string> CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
{{
return ctx.CallActivityAsync<string>(""MyActivity"", input, options);
}}

[Function(nameof(MyActivity))]
public static async Task<string> MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
{{
ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance<MyNS.MyActivity>(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<MyNS.MyEntity>();
}}
{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
}}",
isDurableFunctions: true);

await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
Expand Down
13 changes: 8 additions & 5 deletions test/Generators.Tests/Utils/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,14 @@ static class TestHelpers
public static Task RunTestAsync<TSourceGenerator>(
string expectedFileName,
string inputSource,
string expectedOutputSource,
string? expectedOutputSource,
bool isDurableFunctions) where TSourceGenerator : IIncrementalGenerator, new()
{
CSharpSourceGeneratorVerifier<TSourceGenerator>.Test test = new()
{
TestState =
{
Sources = { inputSource },
GeneratedSources =
{
(typeof(TSourceGenerator), expectedFileName, SourceText.From(expectedOutputSource, Encoding.UTF8, SourceHashAlgorithm.Sha256)),
},
AdditionalReferences =
{
// Durable Task SDK
Expand All @@ -35,6 +31,13 @@ public static Task RunTestAsync<TSourceGenerator>(
},
};

// 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
Expand Down
Loading